7.2 The CSRF check: CsrfViewMiddleware.process_view
CsrfViewMiddleware is the CSRF check: before a view runs, it decides whether a request of a method that is not safe came from the site’s own pages, first by its "Origin" or "Referer" header and then by a token that must match the browser’s CSRF secret. A request it refuses is answered by the view that CSRF_FAILURE_VIEW names, csrf_failure by default, which returns a 403.
The check is a sequence of tests, and the first that decides ends it. Four let a request through without looking at it; the rest ask where it came from and what it carries. Each refusal has a reason, a short sentence that is logged and handed to the failure view. An accepted request goes on to its view; a refused one gets the failure view’s response in its place. The secret and its token are in The CSRF secret and its token.
CsrfViewMiddleware.process_view , from the top. The first test that decides ends the check.Requests let through unchecked
A request already checked is not checked again. CsrfViewMiddleware marks an accepted request with the attribute "csrf_processing_done", and process_view returns at once for a request so marked, which is what lets the middleware and the csrf_protect decorator share one request (A middleware as a decorator).
A view marked by csrf_exempt is let through next, without the request being marked (View decorators). It is tested after CsrfViewMiddleware has read the browser’s secret, so that, a comment says, get_token still works in an exempt view.
Then the safe methods, "GET", "HEAD", "OPTIONS" and "TRACE", on the comment’s assumption that whatever RFC 9110 does not define as safe needs protection. Last, the test client’s flag: its handler sets "_dont_enforce_csrf_checks" on every request, true unless the client was made with enforce_csrf_checks=True (The test client: Client and ClientHandler). Both these paths mark the request as checked.
The origin
A request with an "Origin" header is refused unless CsrfViewMiddleware accepts it, in one of three ways tried in order. The header may equal the site’s own origin: "https" or "http" as HttpRequest says, and the host from HttpRequest (Headers, host and scheme), a host refused by ALLOWED_HOSTS raising DisallowedHost, which is passed over. It may be one of CSRF_TRUSTED_ORIGINS exactly. Or it may have the scheme of a trusted origin written with a "*", and a host that matches that origin’s host, star taken off, by is_same_domain, where a pattern that begins with a dot matches the domain and every subdomain.
The setting is read into three cached properties, once for each instance of the middleware. In a run, a client whose chain had refused an origin went on refusing it after CSRF_TRUSTED_ORIGINS was changed to name it; a new client accepted it.
Only a request with no "Origin" has its "Referer" read, and only if it is secure. The comment’s reason: an attacker in the middle of a plain-HTTP visit can supply a cookie and a token of their own, the secret being tied to no session, and post them to the HTTPS site, which must treat its plain-HTTP self as untrusted; and as Barth et al. found the "Referer" missing from same-domain HTTPS requests in about 0.2% of cases or fewer, a strict check is possible.
CsrfViewMiddleware refuses a request with no "Referer", one that does not parse into a scheme and a host, and one whose scheme is not "https". It accepts a host that matches a trusted origin’s host by is_same_domain, whatever that origin’s scheme. Otherwise the host must match the cookie’s domain, CSRF_COOKIE_DOMAIN or, with CSRF_USE_SESSIONS, SESSION_COOKIE_DOMAIN, with the request’s port added unless it is 443 or 80; or, when that domain is None, as by default, get_host, port and all, a DisallowedHost being refused.
With CSRF_TRUSTED_ORIGINS = ["https:// added to the harbour’s, a run of the example, with the test client making the requests, each with the cookie and the token, gave:
POST https://harbour.example/berths/
Origin: https://pilots.harbour.example 200 Berth requested.
Origin: http://pilots.harbour.example 403 Forbidden (Origin checking failed - http://pilots.harbour.example does not match any trusted origins.): /berths/
Origin: https://a.pay.example 403 Forbidden (Origin checking failed - https://a.pay.example does not match any trusted origins.): /berths/
Referer: https://harbour.example:8443/berths/
403 Forbidden (Referer checking failed - https://harbour.example:8443/berths/ does not match any trusted origins.): /berths/
The token
CsrfViewMiddleware reads the secret again from the cookie or the session, by CsrfViewMiddleware, rather than taking the copy process_request kept in META. The comment’s reason: an authentication middleware may have called rotate_token in its process_request, replacing that copy, while the browser’s token was made from the old secret. A malformed cookie is refused, though process_request has already replaced it for the response. No secret is refused too: insisting on a cookie for a "POST", the comment says, avoids all CSRF attacks, login CSRF among them.
For a "POST" the token is the form field "csrfmiddlewaretoken". An UnreadablePostError while the body is read is swallowed, as process_view should not raise, the comment says, and the field counts as empty. If the field is missing or empty, or the method is another, the token is the header CSRF_HEADER_NAME names, "HTTP_X_CSRFTOKEN" by default, which is X-CSRFToken: a fallback, the comment says, to make things easier for AJAX and possible for "PUT" and "DELETE". No other body is read: in a run, a "PUT" whose form-encoded body held a good token was refused with "CSRF token missing.".
The token must then be 32 or 64 letters and digits. _does_token_match unmasks a token of 64, takes one of 32 as the secret itself, as a script that read the cookie sends it, and compares it with the secret by constant_time_compare (Secret keys and django). CsrfViewMiddleware names a failing token’s source: "POST", or "the 'X-Csrftoken' HTTP header", as HttpHeaders spells it.
Refusal and its reasons
The referer and token tests raise RejectRequest with their reason, and every refusal ends in CsrfViewMiddleware. It imports the view CSRF_FAILURE_VIEW names by get_callable, calls it with the request and the reason, and logs the response by log_response to "django, as "Forbidden (%s): %s" with the reason and the path. log_response logs a 4xx as a warning and marks the response as logged, so the handler, which logs every response of 400 or more to "django.request", does not log it again.
A comment says _accept and _reject exist as methods for requires_csrf_token, whose class replaces _reject with one that refuses nothing (A middleware as a decorator). The security checks confirm that the failure view can be imported and takes a request and a reason (The security checks).
REASON_INCORRECT_LENGTH and REASON_INVALID_CHARACTERS.| reason | refused when | message |
|---|---|---|
REASON_ |
refused when the "Origin" is not verified |
message "Origin checking failed - %s does not match any trusted origins." |
REASON_ |
refused when secure, and no "Origin" or "Referer" |
message "Referer checking failed - no Referer." |
REASON_ |
refused when the "Referer" does not parse into a scheme and a host |
message "Referer checking failed - Referer is malformed." |
REASON_ |
refused when the "Referer" is not HTTPS |
message "Referer checking failed - Referer is insecure while host is secure." |
REASON_ |
refused when the host is neither the site’s nor trusted | message "Referer checking failed - %s does not match any trusted origins." |
| a malformed cookie | refused when its length or characters | message "CSRF cookie has incorrect length.", "CSRF cookie has invalid characters." |
REASON_ |
refused when no secret | message "CSRF cookie not set." |
REASON_ |
refused when no token | message "CSRF token missing." |
| a malformed token | refused when its length or characters | message "CSRF token from POST has incorrect length." |
| a wrong token | refused when it does not match the secret | message "CSRF token from POST incorrect." |
The failure view: csrf_failure
csrf_failure builds its page’s context: a translated title and sentence; the reason; "no_referer" and "no_cookie", true when the reason is REASON_NO_REFERER or REASON_NO_CSRF_COOKIE, each with paragraphs for the user; settings.DEBUG, with the documentation’s version for its links and a line for when it is off; and the nonce, merged in from the csp context processor.
It asks the project’s template engines for "403_csrf.html". A project’s template of that name is rendered with the request alone and none of that context: in a run, one that wrote {{ reason }}, {{ no_cookie }} and {{ csp_nonce }} rendered the first two empty and the third, from the context processor, with the nonce. When no engine has it, csrf_failure renders the built-in "csrf_403.html" with an Engine of its own and the context, so the page does not depend on TEMPLATES. A template name of a caller’s own that does not exist raises TemplateDoesNotExist.
The built-in page shows the paragraphs for a missing "Referer" or cookie when the reason is one of those, and, with settings.DEBUG true, the reason and a list of what to check. Its stylesheet carries {% csp_nonce_attr %}, so the harbour’s 403 made the nonce, and its policy named it, though only in "script-src", where the harbour’s setting puts it. The response is an HttpResponseForbidden.
CsrfViewMiddleware sets async_capable = False, so under ASGI it runs as a synchronous layer. The check is synchronous for another reason: process_view is a synchronous method, which the handler adapts to its own mode separately from the layer (Sync and async in one chain).