SuspiciousOperation Invalid HTTP_HOST header with Django

I was recently getting this error while trying to deploy a new project to production. It turns out that due to a recent security update, you need to make sure that the incoming host name in your request is valid.

Update

This original post was overlooking a more important security issue so I've updated the post with an explanation while leaving the original post below

SuspiciousOperation: Invalid HTTP_HOST header (you may need to set ALLOWED_HOSTS): foo_bar

In the original post below, I mentioned that due to updates in how host names are parsed by Django, if your host name had an underscore it would reject the host name and throw a 400 bad request. Changing the underscore to a hyphen would fix the problem.

While this is true, it's overlooking a more important issue. Django has a well documented ALLOWED_HOSTS setting that needs to be set in production to reflect the host names of the application you are serving. If this setting isn't defined, or if the host name in the incoming request doesn't match one in your ALLOWED_SETTINGS tuple then you will get a 400 exception and a nice error message saying:

SuspiciousOperation: Invalid HTTPHOST header (you may need to set ALLOWEDHOSTS): ...

If you are using NGINX you are most likely passing requests to your Python application server using proxy_pass. By default, NGINX will rewrite the incoming HTTP host header. This means if the host header was originally example.com and your NGINX server block is something like:

location / {
 proxy_pass http://example-appserver;
}

Then the HTTP header will be rewritten from example.com to example-appserver. As the below post explained, you would have to then make sure your ALLOWED_HOSTS included:

ALLOWED_HOSTS = ['example-appserver', ]

This undermines the whole point of the ALLOWED_HOSTS setting in the first place. Because NGINX is rewriting the host header, it doesn't matter what it was originally set to. This means that any bot or script that is trying to spoof the host header will be allowed through.

To prevent this, you should always include the following in your NGINX config:

proxy_set_header Host $http_host;

Which will pass the original host header onto the Python application server.

Original Post

I was recently deploying a project with Gunicorn, NGINX & Django (1.4.5). With everything set up for production, I was continually getting the following error when trying to access any URL on my site:

SuspiciousOperation: Invalid HTTP_HOST header (you may need to set ALLOWED_HOSTS): foo_bar

After Google'ing around briefly, it became clear that a recent security update (Feb 2013) has made an important change with regard to how you deploy your project.

A new settings ALLOWED_HOSTS has been introduced. When you are in DEBUG=True this setting is ignored altogether but when you switch to production you have to make sure that the HTTP_HOST value in the incoming request header matches a value in ALLOWED_HOSTS setting:

This is a security measure to prevent an attacker from poisoning caches and password reset emails with links to malicious hosts by submitting requests with a fake HTTP Host header, which is possible even under many seemingly-safe webserver configurations.

If you are using NGINX, you need to be aware that NGINX is implicitly rewriting the HTTP_HOST header before it reaches your application server. In other words, with the default NGINX config, your Django application will only see the host name set by your NGINX proxy, not the original host header as sent by the original user.You can easily change this by adding proxy_set_header Host $http_host; to your server block - here is a good blog post with more information.

My NGINX config was similar to the following:

upstream gunicorn_mysite{
    ip_hash;
    server 127.0.0.1:50000;
}
server {
  ...
  location / {
    proxy_pass              http://gunicorn_mysite;
  }
  ...
}

The HTTP_HOST header was arriving to the application server as gunicorn_mysite. If you read the security notes on how Django validates the HTTP_HOST header you will see the following:

Hostnames must consist of characters [A-Za-z0-9] plus hyphen ('-') or dot ('.').

Notice there is no mention of _ (underscore). This underscore in my host name was making the Django's validation fail and raise an exception. By simply changing gunicorn_mysite to gunicorn-mysite fixed the problem and I no longer received the error.