Use Django ALLOWED_HOSTS to Prevent Security Threats

At some point, you may have come upon the Django ALLOWED_HOSTS feature. Correctly configuring this Django setting can prevent many potential security vulnerabilities like HTTP Host header attacks. In this article, I’ll explain why this feature is important, what kind of threats it can prevent and how to properly configure it.

Django ALLOWED_HOSTS explained

ALLOWED_HOSTS is a list defined in the project’s settings.py file. It contains hostnames or domain names that Django can serve, or in other words, it denotes the hostnames that your server will listen to. Why is this even important?

To answer this question, we first have to dive into how web servers work and what are the potential risks.

HTTP Host header

Nowadays, it’s common for the same web server to host multiple sites on the same IP address. To route the client request to the correct site, the Host header needs to be defined. If the Host header was not defined, the server would not know to which site it should forward the request. An example HTTP request sent to https://zerotobyte.com/about/ may look like this:

GET /about HTTP/1.1
Host: zerotobyte.com
NOTE - As of HTTP/1.1, the HTTP Host header is a mandatory request header.

Potential risks of relying on the HTTP Host header

Since the Host header is controllable and can be manually set by a client sending the request, this introduces some unwanted security vulnerabilities. By inserting the malicious domain in the Host header, attackers could do harm in many ways. One way of doing it is by defining the X-Forwarded-Host header that can overwrite the Host header. An example of an HTTP request sent by the attacker may look like this:

GET /about HTTP/1.1
Host: zerotobyte.com
X-Forwarded-Host: attacker.com

This type of attack is also commonly known as the HTTP Host header attack, part of a group of HTTP header injection attacks. Attackers most commonly use this technique for:

  • Web-cache poisoning, in which the attacker manipulates web-cache and poisoned content is served to subsequent visitors.
  • Abusing the channels for conducting sensitive operations such as password resets.
  • Poisoning links in the email that are constructed by using the Host header, etc.
NOTE - X-Forwarded-Host support is disabled by default in Django. To enable it set the USE_X_FORWARDED_HOST to True.

To illustrate the danger, I’ll explain how web-cache poisoning works. If the site uses CDN (Content Delivery Network) to deliver content over the web, it means most of the time cached content is downloaded to browsers. If no validation mechanism exists, the attacker can inject malicious domain via Host header to any dynamically generated link on the site. CDN can then pick it up, which means that the malicious content could remain in the cache for a while. Any subsequent requests to the site will fetch the poisoned content from the cache, spreading the infection even further.

Django ALLOWED_HOSTS as a security mechanism

Now it should be clear why Django introduced the ALLOWED_HOSTS feature. It is a security mechanism used to prevent attackers from submitting requests with fake HTTP Host headers. When hostnames are defined inside ALLOWED_HOSTS, the received Host header is validated against those hostnames.

If the received Host header does not match any of the ALLOWED_HOSTS values, Django raises the SuspiciousOperation exception. The validation is performed during Django’s get_host() method execution.

WARNING - If you use alternative methods of retrieving the host, such as accessing the request.META directly, the validation will not be applied. Only by using the get_host() method the Host header is validated.

Examples of ALLOWED_HOSTS configuration

As already mentioned before, the ALLOWED_HOSTS list accepts values that represent host/domain names. The values can be fully qualified domain names (FQDN) with wildcard support for subdomains or part of domain names.

If Debug is set to True, and ALLOWED_HOSTS is empty, the host is validated against ['.localhost', '127.0.0.1', '[::1]'].

There are multiple ways of defining the allowed hosts in Django. You can set it as:

  • A fully qualified domain name that will be matched against the request’s Host header precisely as it is:
    ALLOWED_HOSTS = [‘www.zerotobyte.com’]
  • The IP address of the host machine (in this example it’s localhost but can be any other IP address too):
    ALLOWED_HOSTS = [‘127.0.0.1’]
  • Subdomain wildcard that will allow any subdomain host (e.g., www.zerotobyte.com, app.zerotobyte.com, etc.):
    ALLOWED_HOSTS = [‘.zerotobyte.com’]
  • * wildcard that will allow whatever hostname comes in a Host header:
    ALLOWED_HOSTS = [‘*’]

If you do not set ALLOWED_HOSTS values correctly, accessing the deployed site URL will result in Django producing the DisallowedHost error. The error will usually look like this:

DisallowedHost at /
Invalid HTTP_HOST header: 'XXX.XXX.XXX.XXX:8000'. You may need to add u'XXX.XXX.XXX.XXX' to ALLOWED_HOSTS.

For testing things out in the development environment, you can set the * wildcard to the ALLOWED_HOSTS. But as soon as you start preparing the configuration for the production environment, remember to explicitly specify the hostname you’d like to whitelist.

Best practices to minimize the security risks

I compiled some best practices for hardening your Django site security related to host validation.

Never leave ALLOWED_HOSTS empty

You should never leave the ALLOWED_HOSTS list empty. Even when Debug is set to True, my recommendation is to configure the ALLOWED_HOSTS appropriately just to create a habit out of it. Explicitly whitelist whatever domains you trust to, and close the doors for any other potential threats. Also, when dynamically generating links inside your site, do it by using Django’s get_host() method. Avoid accessing the Host header directly through request.META attribute.

Reconsider enabling support for X-Forwarded-Host header

Doublethink if you want to enable Django’s support for X-Forwarded-Host headers via the USE_X_FORWARDED_HOST attribute. Sometimes, this header can cause trouble because it can override the Host header, and you may be asking where’s the problem.

Protect absolute URLs

Try not to expose absolute URLs inside your site. Double-check whether each URL needs to be absolute. If you need to have absolute URLs on your site, ensure that the domain is manually specified in the configuration.

Conclusion

Django web framework is equipped with all sorts of security utils and mechanisms to help you breathe easier. ALLOWED_HOSTS feature is just one of these, and it can prevent all kinds of security vulnerabilities you don’t even know exist. Mastering these Django’s features will make your sites and APIs more prone to any breaches and malicious operations.

Still, you can’t rely only on these built-in mechanisms. You must know what are the most common threats in general and how to prevent them. Join communities that talk about it, learn it in theory, and get some hands-on experience preventing these threats. And most important, keep reading this blog to learn more stuff like this. See you next time! 🙂