Fixing Django "DisallowedHost" in Production
Recently, I’ve been creating and deploying Django based applications. It’s been a smooth experience overall but there are some gotchas and some specialized knowledge that is needed. This was something I learned when I started to get bombarded with “DisallowedHost” Exceptions in my email inbox.
Host Header Attack Vector
One thing that Django has you set up is Allowed Hosts to your application. This is because of something called a host header attack or host header injection. Bad people across the internet will send malicious requests to your webserver in an attempt to backdoor it.
Heres how to recreate this type of attack with curl
:
curl https://mywebsite.com --header "Host: fasfsadfs.com"
You should recieve a 500 response and get an email about and Django exception saying “DisallowedHost”.
What’s going on in Django
If you look at your settings.py
in your Django application, you should see the following configuration:
ALLOWED_HOSTS = ['mywebsite.com', 'www.mywebsite.com']
So Django recieves the request with a hostname that does not match the configuration. Since this host is not allowed, Django will generate an exception. This is actually good behavior since we don’t want to let these malicious requests go through to our application.
However, generating exceptions in Django should be exceptional. This is for a few reasons, heres a few I can think:
- You get pestered with emails (annoying but not the end of the world)
- Large logs can build up and other exceptions can be harder to find (again annoying but storage is cheap right??)
- Performance across the whole application can take a hit if we’re generating exceptions that we don’t have to😳
We need a “cheaper” solution for requests that contain an invalid host.
Introducing Nginx Webserver
Most likely you’re already using a web server to front your Django application. If not, I would advise you to do so as you can take advantage of performance benefits of doing so and to also solve this problem. I use Nginx as my webserver, so the rest of the post will be talking about an Nginx based solution.
Since we have our Nginx webserver set up, we should take advantage of it’s high performance properties to catch and deal with requests that contain an invalid host. The gain here is two fold, no more annoying messages and our more expensive python process can focus on serving legitamate users.
Configuring Your Nginx Webserver
Let’s get our hands dirty and dive into some Nginx configurations. I’m going to be walking through it step by step, if you already have Nginx experience you may just want to skip to step 3 to ge the fix.
Step 1: Create a server block
This is the most basic configuration, we need to tell Nginx to listen on a port(80 for http traffic and 443 for https traffic) and tell it what our server name is. We’re also adding our www
subdomian so we have a more complete example if you’re dealing with subdomains in your application. Feel free to remove it if you don’t care about exposing it. Create a file called nginx.conf
(I guess you can call it something else but that’s how I name my Nginx cofigs) and add the following.
server {
listen 80 443;
server_name mywebsite.com www.mywebsite.com;
}
Step 2: Reverse proxy our Django Service
Okay, so now we need to tell Nginx to route our requests to our Django application via a location
block. The configurations in the location
block I’ve have are pretty important, if you don’t include them you may run into other issues. If you’re going to change them, hopefully you know what you’re doing.
server {
listen 80 443;
server_name mywebsite.com www.mywebsite.com;
location / {
# This should be where your Django app is being served.
proxy_pass http://localhost:8000;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
}
Step 3: Whitelist Hostnames
Our strategy to deal with the problamatic requests is to whitelist domains that are allowed in Nginx. Requests that contain hosts that not included in this whitelist will not be allowed to hit the Django application. What’s cool is Nginx can define a function like declaration and allows for conditional logic. To implement this, it is two step process:
Define a map in Nginx that we can check our whitelisted domains. It’s going to look something like this
map $http_host $is_trusted { default 0; "mywebsite.com" 1; "www.mywebsite.com" 1; }
Preform logic to check the map and return a
444
response if its a bad request. That looks like the following:if ($is_trusted = 0) { return 444; # No response to untrusted hosts }
Given those two pieces of information, let’s add them to our config:
map $http_host $is_trusted {
default 0;
"mywebsite.com" 1;
"www.mywebsite.com" 1;
}
server {
listen 80 443;
server_name mywebsite.com www.mywebsite.com;
if ($is_trusted = 0) {
return 444;
}
location / {
# This should be where your Django app is being served.
proxy_pass http://localhost:8000;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
}
But Does it Work?
Okay so now that we got our configuration set up. Let’s go ahead and test to verify that its working.
Go ahead and whip up our curl
request:
curl https://mywebsite.com --header "Host: fasfsadfs.com"
You should not get a response, since Nginx just terminate the request. In addition, there will be no exception email that get’s sent. That’s excellent news.
A few things to be on the look out for:
- This solution may not work or be combersome for you if you have a bunch of domains that you need to whitelist. You can do something like template and generate the Nginx config.
- Watch out if your locally testing. You will need to change the allowed hosts to
locahost
for both the Django and Nginx config.
It’s not hard to set this up. It just requires a bit of knowlege of Nginx. Now we can move onto improving our application.