Note: is the documentation of the
verbose_csrf_middleware
package, a tool of general utility for Django developers. It lives here on the Bugsink website because [1] it’s created by Bugsink and [2] it’s used by Bugsink to help people self-hosting Bugsink with debugging any CSRF issues.
CSRF protection is a security feature in Django that helps prevent Cross-Site Request Forgery attacks. The basic idea behind Django’s CSRF protection is that the server generates a token that is sent to the client. This token must be sent back to the server with every POST request. The server then checks that the token is correct.
However, in Django 4.0 additional checks were added to the CSRF protection: When Origin
and/or Referer
headers are
present in the request, Django will also check that these match the host that the request is made to. It is these checks
that can cause issues with reverse proxies.
verbose_csrf_middleware
is a verbatim copy of the Django CSRF middleware, but it is more verbose in its failures.
This is especially useful when CSRF failures are happening due to some misconfiguration of your server, your reverse
proxy, or some combination thereof.
pip install verbose_csrf_middleware
In your settings.py
file, in the MIDDLEWARE_CLASSES
, search for this line:
'django.middleware.csrf.CsrfViewMiddleware', # search this to remove it
and then replace it with the line below:
'verbose_csrf_middleware.CsrfViewMiddleware',
You’ll probably want to see the output of the middleware somewhere. You can either:
DEBUG
"django.security.csrf"
(level: warning) end up in a location you can read.403_csrf.html
to your templates directory. Make sure the template renders "reason"
.CSRF_FAILURE_VIEW
Note that optinos 1, 3 and 4 have at least theoretical security implications, because by the nature of “verbose” they expose some information to end-users.
Now that you have the middleware installed, you can see the output of the middleware in your logs, or right in your browser if you’ve set up the template or view. Next step: understanding the messages.
Here’s the most common error message you’ll see:
Origin header does not match (deduced) Host:
'http://xxx' != 'http://yyy'
Django tries to match the value in the Origin
header with what Django thinks is the
Host
(the domain your site is running on). And fails at that.
The first displayed value, in the above: http://xxx
, is
the Origin
header as Django sees it. It should match the site the request was sent from.
In the typical case: it should match the domain of your site.
If it doens’t, it is because:
Origin
header (e.g. a reverse proxy). this is more
likely, and you should check your reverse proxy settings.The second displayed value, in the above http://yyy
, is the Host
that Django
thinks the site is running on. This is deduced from the Host
header in the request,
or if you’re using USE_X_FORWARDED_HOST
, from the X-Forwarded-Host
header. If this
is wrong, you should check what your reverse proxy is sending as the Host
header.
And if it’s sending X-Forwarded-Host
, whether USE_X_FORWARDED_HOST
is set to True
.
If the scheme of the Origin
header doesn’t match the scheme of the Host
header, but the
hostnames do match, you’ll see the following message:
Origin header does not match (deduced) Host:
'http://xxx' != 'http://yyy';
(wrong scheme)
This typically happens when Django thinks it’s running on plain HTTP, but it’s actually being accessed over HTTPS. This is a common issue with reverse proxies.
The solution is to make sure your reverse proxy is sending the correct X-Forwarded-Proto
header.
(In principle, this message could also be shown when you have actually a site that’s running on both HTTP and HTTPS, and are posting from the former to the latter. But this is a rare setup, and you should probably just use HTTPS everywhere.)
If CSRF_TRUSTED_ORIGINS
is set in your settings, the error message will be more
verbose:
Origin header does not match (deduced) Host:
'http://xxx' != 'http://yyy';
nor any of the CSRF_TRUSTED_ORIGINS: ['http://zzz']
This message speaks for itself. In short: it shows the CSR_TRUSTED_ORIGINS
setting
as parsed by the middleware. It also explicitly points out scheme-mismatches if they
occur as “(wrong scheme)
”.
If no Origin
header is present, Django will check the Referer
header. If there’s
an error there, you’ll see a message like this:
Referer checking failed - 'xxx' does not match any of
['yyy' (host), 'zzz' (trusted)].
The first displayed value, in the above: xxx
, is
the domain forom the Referer
header as Django sees it. It should match the site the
request was sent from. In the typical case: it should match the domain of your site.
If it doens’t, it is because:
Referer
header (e.g. a reverse proxy). this is more
likely, and you should check your reverse proxy settings.The list of values in the above ['yyy' (host), 'zzz' (trusted)]
is the list of
things Django is checking the Referer
against. What these things are is explained
in parentheses. The possible values are:
host
: what Django thinks is the Host
your site is running on.
This is deduced from the Host
header in the request,
or if you’re using USE_X_FORWARDED_HOST
, from the X-Forwarded-Host
header. If this
is wrong, you should check what your reverse proxy is sending as the Host
header.
And if it’s sending X-Forwarded-Host
, whether USE_X_FORWARDED_HOST
is set to True
.
trusted
: from settings.CSRF_TRUSTED_ORIGINS
session_cookie
: from settings.SESSION_COOKIE_DOMAIN
(used because settings.CSRF_USE_SESSIONS == True
)csrf_cookie
: from settings.CSRF_COOKIE_DOMAIN
(used because settings.CSRF_USE_SESSIONS == False
)verbose_csrf_middleware
is a verbatim copy of Django 4.2’s csrf middleware, with changes for verbosity. Because there
there were no recent changes to that module, the package is compatible with:
Now that you’re here: Bugsink is a simple, self-hosted error tracking tool that’s API-compatible with Sentry’s SDKs. Check it out