'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
RefererandHostmatch unless you specify aCSRF_TRUSTED_ORIGINS(see the code aroundREASON_BAD_REFERERhere) - If you don't specify
CSRF_TRUSTED_ORIGINS, the system falls back onrequest.get_host() request.get_host()usesrequest._get_raw_host()request._get_raw_host()checks sequentiallyHTTP_X_FORWARDED_HOST(ifUSE_X_FORWARDED_HOSTis set),HTTP_HOST, andSERVER_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 toX-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
portfromX-Forwarded-Hostin nginx configuration (on the assumption that the non-specX-Forwarded-Hostfollows the same semantics asHost)
To avoid hard-coding domains in CSRF_TRUSTED_ORIGINS, the second option is attractive, but it may come with security caveats. Speculatively:
X-Forwarded-Protoshould 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-Protomight 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 |
