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.
Debug is set to
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.,
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
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.
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! 🙂