Protecting your web applications from the Host
header vulnerability injection is crucial in maintaining the security and integrity of your web infrastructure. This type of vulnerability can be exploited by attackers in several ways, including password reset poisoning and web cache poisoning, potentially leading to a wide range of attacks.
Fortunately, when using NGINX, several configurations can be implemented to mitigate such risks.
Understanding cache poisoning via Host
header injection
Cache poisoning through the Host
header injection is a sophisticated attack where an attacker manipulates the cache of a web application by injecting malicious data into the cache through the Host
header. This manipulated cache can then serve malicious content to legitimate users. Mitigating this attack in NGINX involves ensuring that only valid Host
headers are used in responses that get cached, and any request with a suspicious or manipulated Host header does not poison the cache.
Example Usage of curl
for Testing Host Header Injection
The following command can be used to send a request to a server with a custom Host
header. This is a common technique used in security assessments to determine if a server might be improperly handling the Host
header, potentially making it vulnerable to attacks like cache poisoning or redirection hijacks:
curl -v -H "Host: malicious.host" https://example.com/
-v
enables verbose mode, which outputs detailed information about the request and response, aiding in the analysis.-H "Host: malicious.host"
sets a customHost
header for the request. In a security assessment, themalicious.host
value would be replaced with a domain or value controlled by the researcher or a harmless value designated for testing purposes.- `https://example.com/` is the URL of the the website you own.
A vulnerable server will respond with HTTP/1.1 200 OK
for such requests, meaning it accepts an arbitrary Host
header value. This allows an attacker to send unlimited number of various Host:
header values in their requests, ultimately increasing cache storage of the server to entirely fill the disk, or causing early purging of the cache, degrading performance.
The correct behavior would be to deny requests containing untrusted Host
header value.
There are two different approaches to protect an NGINX server from the Host
header injection vulnerabilities.
The approach with whitelisting known domains
In this approach, you can configure NGINX to mitigate cache poisoning attacks via Host
header injections, by listing all the default domains on your server.
What are the default domains? Those are any of your own domains listed in your NGINX configuration alongside default_server
flag. In other words, you only need to whitelist domains which are normally served when Host
header is not provided at all. Or, in other words, when your website is being accessed directly by the IP address of the server. They are so-called “main” domains for your server.
It is also the best practice to set default_server
in your NGINX configuration:
For example:
listen 80 default_server;
listen 443 ssl default_server;
...
server {
server_name www.example.com example.com ;
...
}
Here, example.com
is the primary website on your server. There may be more website on your server, but they can’t be designated as primary, and must not have the default_server
flag set, as it’s unique per listening IP:port combination.
Step 1: Define Trusted Hosts
Start by defining the set of the main/trusted hosts for your application. In a real-world scenario, these would be the domains your application is intended to serve.
map $http_host $is_trusted {
default 0;
"www.example.com" 1;
"example.com" 1;
}
This map
block maps trusted Host
headers to a variable $is_trusted
, marking them with 1
for trusted and 0
for all others.
Step 2: Block or Redirect Untrusted Host Headers
Using the $is_trusted
variable, we can configure NGINX to block or redirect requests with untrusted Host
headers, preventing them from poisoning the cache.
server {
listen 80;
server_name www.example.com example.com;
if ($is_trusted = 0) {
return 444; # No response to untrusted hosts
}
# Your usual server configuration
}
Caveats
While this approach effectively protects your server from the Host
header vulnerability, it requires explicitly whitelisting domains manually. However, if you manage your NGINX configuration via configuration tools like ansible
, it becomes a non-issue.
The added benefit of this approach is that your server will work perfectly fine with web clients which do not support SNI. More on that later.
Approach with the catch-all default_server
website
A default server block can catch all other requests that do not match server_name
of any other website. It is the easiest way to protect from the Host
header vulnerability injection in NGINX:
server {
listen 80 default_server;
return 444;
server_name _;
}
Now any requests with an arbitrary (not listed elsewhere) Host:
header will reach this denying server
block. Make sure to remove default_server
flag from other websites.
Caveats
The approach, while most elegant, does not account for web clients which do not support SNI. When such clients genuinely load your website over secure connection, e.g. by opening “https://example.com/”, NGINX does not see any Host
header, and because of this, the request will end up in the catch-all server block. And due to this, non-SNI supporting clients will not be able to load any websites on your server.
Despite widespread support, there are still areas where SNI clients face limitations or special considerations:
- Legacy Systems and IoT Devices: Some older systems or IoT (Internet of Things) devices may not support SNI due to outdated software or limited capabilities. This can affect their ability to connect to specific services hosted on shared servers requiring SNI for proper routing.
- Privacy Concerns with SNI: Traditional SNI exposes the hostname in plaintext during the TLS handshake, which can be a privacy concern. Encrypted SNI (ESNI) or its successor, Encrypted Client Hello (ECH), address this by encrypting the portion of the handshake that contains the hostname. However, adoption rates and support for ESNI/ECH can vary, impacting privacy.
- Networking and Security Appliances: Certain middleboxes (like firewalls and load balancers) and older security appliances may have incomplete or outdated support for SNI, affecting their ability to route or inspect HTTPS traffic correctly. This can require updates or configuration changes to ensure compatibility.
Recommendations
To avoid inadvertently blocking legitimate clients while still protecting against Host
header injection attacks, consider using the first approach outlined in this article, while ensuring best practices:
- Configure server blocks for all known, legitimate hostnames your server is intended to serve. This ensures that any SNI supporting clients can establish a connection to desired websites
- Configure the primary website on your server, with
default_server
flag alongside itslisten
directive. This ensures that any client which does not support SNI can access the primary website - If support for outdated non-SNI clients is crucial and you want more than just primary website to work for those, you must use multiple IP addresses in your server and NGINX configuration
For these reasons, when configuring NGINX to mitigate vulnerabilities, including Host header injections, it’s crucial to consider the behavior of clients that do not support Server Name Indication (SNI).
Conclusion
While NGINX does not directly mitigate the Host
header injections out of the box, through careful configuration and adherence to best practices, you can fully protect your applications from such attacks. It’s a critical part of maintaining the security posture of your web applications, ensuring they only respond to legitimate and recognized Host
headers.