Verbose CSRF Middleware
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.
What is CSRF?
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
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.
Installation:
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',
Seeing the output
You’ll probably want to see the output of the middleware somewhere. You can either:
- Turn on
DEBUG
- Make sure messages to the logger
"django.security.csrf"
(level: warning) end up in a location you can read. - Add a template
403_csrf.html
to your templates directory. Make sure the template renders"reason"
. - Add a
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.
The messages
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.
Origin header does not match (deduced) Host
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:
- Browser bug/extenstions: Your browser is not sending the right information (unlikely, but easy to check in the browser’s dev tools), so probably a good first step
- Reverse Proxy misconfiguration: Something is rewriting the
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
.
… (wrong scheme)
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.)
… nor any of the CSRF_TRUSTED_ORIGINS
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)
”.
Referer checking failed
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:
- Your browser is not sending the right information (unlikely, but easy to check in the browser’s dev tools), so probably a good first step
- Something is rewriting the
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 theHost
your site is running on. This is deduced from theHost
header in the request, or if you’re usingUSE_X_FORWARDED_HOST
, from theX-Forwarded-Host
header. If this is wrong, you should check what your reverse proxy is sending as theHost
header. And if it’s sendingX-Forwarded-Host
, whetherUSE_X_FORWARDED_HOST
is set toTrue
. -
trusted
: fromsettings.CSRF_TRUSTED_ORIGINS
session_cookie
: fromsettings.SESSION_COOKIE_DOMAIN
(used becausesettings.CSRF_USE_SESSIONS == True
)csrf_cookie
: fromsettings.CSRF_COOKIE_DOMAIN
(used becausesettings.CSRF_USE_SESSIONS == False
)
Compatability
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:
- Django 4.2
- Django 5.0
- Django 5.1
Links
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