'Django CSRF Error Casused by Nginx X-Forwarded-host

I've been working on a django app recently and it is finally ready to get deployed to a qa and production environment. Everything worked perfectly locally, but since adding the complexity of the real world deployment I've had a few issues.

First my tech stack is a bit complicated. For deployments I am using aws for everything with my site deployed on multiple ec2's backed by a load balancer. The load balancer is secured with ssl, but the connections to the load balancer are forwarded to the ec2's over standard http on port 80. After hitting an ec2 on port 80 they are forwarded to a docker container on port 8000 (if you are unfamiliar with docker just consider it to be a standard vm). Inside the container nginx listens on port 8000, it handles a redirection for the static files in django and for web requests it forwards the request to django running on 127.0.0.1:8001. Django is being hosted by uwsgi listening on port 8001.

server {
    listen   8000;
    server_name localhost;

    location /static/ {
        alias /home/library/deploy/thelibrary/static/;
    }

    location / {
        proxy_set_header X-Forwarded-Host $host:443;
        proxy_pass http://127.0.0.1:8001/;
    }
}

I use X-Forwarded host because I was having issues with redirects from google oauth and redirects to prompt the user to login making the browser request the url 127.0.0.1:8001 which will obviously not work. Within my settings.py file I also included

USE_X_FORWARDED_HOST = True

to force django to use the correct host for redirects.

Right now general browsing of the site works perfectly, static files load, redirects work and the site is secured with ssl. The problem however is that CSRF verification fails.

On a form submission I get the following error

Referer checking failed - https://qa-load-balancer.com/projects/new does not match https://qa-load-balancer.com:443/.

I'm really not sure what to do about this, its really through stackoverflow questions that I got everything working so far.



Solution 1:[1]

For users who cannot use Nginx's built-in facility, here's the root cause:

  • Starting in ~Djagno 1.9, the CSRF check requires that the Referer and Host match unless you specify a CSRF_TRUSTED_ORIGINS (see the code around REASON_BAD_REFERER here)
  • If you don't specify CSRF_TRUSTED_ORIGINS, the system falls back on request.get_host()
  • request.get_host() uses request._get_raw_host()
  • request._get_raw_host() checks sequentially HTTP_X_FORWARDED_HOST (if USE_X_FORWARDED_HOST is set), HTTP_HOST, and SERVER_NAME
  • Most recommended Nginx configurations suggest an entry like proxy_set_header X-Forwarded-Host $host:$server_port;
  • Eventually, the referrer (e.g. <host>) is compared to X-Forwarded-Host (e.g. <host>:<port>). These do not match so CSRF fails.

There isn't a lot of discussion about this, but Django ticket #26037 references RFC2616. The ticket states that a host without a port is "against spec", but that's not true as the spec actually says:

A "host" without any trailing port information implies the default port for the service requested

This leads to (at minimum) the following options (safest first):

  • include host and port in CSRF_TRUSTED_ORIGINS
  • remove port from X-Forwarded-Host in nginx configuration (on the assumption that the non-spec X-Forwarded-Host follows the same semantics as Host)

To avoid hard-coding domains in CSRF_TRUSTED_ORIGINS, the second option is attractive, but it may come with security caveats. Speculatively:

  • X-Forwarded-Proto should be used to clarify the protocol (since the absence of a port implies a default protocol)
  • The reverse proxy MUST use port 443 for HTTPS (i.e. the default for the protocol) and disallow non-HTTPS connection types (X-Forwarded-Proto might fix this).

Solution 2:[2]

I had the same issue running a Django project on GitPod: the X-Forwarded-Host was in the form hostname:443, causing the CSRF error.

I solved it with a custom middleware that strips the port from the header:

# myproject/middleware.py

from django.utils.deprecation import MiddlewareMixin

class FixForwardedHostMiddleware(MiddlewareMixin):
    def process_request(self, request):
        forwarded_host = request.META.get('HTTP_X_FORWARDED_HOST')
        if forwarded_host:
            forwarded_host = forwarded_host.split(':')[0]
            request.META['HTTP_X_FORWARDED_HOST'] = forwarded_host 

To use this middleware, you need to edit your settings.py, and insert the new middleware before the CSRF one, like so:

# myproject/settings.py

MIDDLEWARE = [
     ...
     'myproject.middleware.FixForwardedHostMiddleware',
     'django.middleware.csrf.CsrfViewMiddleware',
     ...
]

See clayton's answer to understand why this fixes the CSRF error.

I don't think this middleware introduces any security issue; please comment if you think otherwise.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 claytond
Solution 2 Benoit Blanchon