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:

  1. Turn on DEBUG
  2. Make sure messages to the logger "django.security.csrf" (level: warning) end up in a location you can read.
  3. Add a template 403_csrf.html to your templates directory. Make sure the template renders "reason".
  4. 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:

  1. 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
  2. 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:

  1. 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
  2. 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 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)

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

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