When running a production web server, it's common to receive requests with malformed or malicious Host headers. These can come from:
- Misconfigured clients or proxies
- Security scanners probing for vulnerabilities
- Old DNS records pointing to your IP
- Attempts at virtual host brute-forcing
A common pitfall occurs when Nginx uses the first server
block as the default catch-all. Here's why this is problematic:
# This becomes the default handler if no Host matches
server {
listen 80 default_server;
# ... this might serve content unexpectedly
}
Here's how to properly implement Host header validation:
server {
listen 80 default_server;
server_name _;
return 444; # Close connection without response
}
server {
server_name www.olddomain.com olddomain.com www.newdomain.com;
listen 80;
return 301 $scheme://newdomain.com$request_uri;
}
server {
server_name newdomain.com localhost;
listen 80;
# Main configuration
}
For enhanced security, consider these additions:
# Block requests with empty Host headers
if ($host = "") {
return 444;
}
# Validate Host against regex pattern
if ($host !~* ^(www\.)?(olddomain|newdomain)\.com$) {
return 444;
}
The 444 response is particularly efficient because:
- No HTTP response is generated
- TCP connection is immediately closed
- Minimal CPU and memory usage
This makes it ideal for handling large volumes of malicious traffic.
To monitor blocked requests:
log_format blocked '$remote_addr - $http_host';
server {
listen 80 default_server;
server_name _;
access_log /var/log/nginx/blocked.log blocked;
return 444;
}
When running a web server, you'll often encounter requests with incorrect or malicious Host headers. These can come from:
- Old DNS records pointing to your IP
- Direct IP access attempts
- Malicious bots scanning servers
- Misconfigured applications
By default, Nginx will process any request reaching the server's IP address, regardless of the Host header. This can lead to:
# Default catch-all behavior
server {
listen 80 default_server;
# This will handle ANY request to the IP
}
Here's how to implement proper Host header validation:
server {
listen 80 default_server;
server_name _;
return 444; # Close connection without response
}
server {
server_name www.olddomain.com olddomain.com www.newdomain.com;
listen 80;
return 301 $scheme://newdomain.com$request_uri;
}
server {
server_name newdomain.com localhost;
listen 80;
# Your actual configuration
root /var/www/html;
index index.html;
# ... other directives
}
For more complex scenarios, you might want to:
# Log invalid host attempts
server {
listen 80 default_server;
server_name _;
access_log /var/log/nginx/invalid_host.log;
return 444;
}
# Handle specific invalid domains differently
map $host $invalid_host {
default 0;
~^(www\.)?badguy\.com$ 1;
}
server {
listen 80;
server_name ~^(www\.)?(old|new)domain\.com$ localhost;
if ($invalid_host) {
return 403 "Forbidden";
}
# Normal processing
}
Verify with curl:
# Valid request
curl -H "Host: newdomain.com" http://your.server.ip
# Invalid request (should get dropped)
curl -H "Host: fake.com" http://your.server.ip
# Check logs
tail -f /var/log/nginx/invalid_host.log
Nginx's 444 is special because:
- It immediately closes the connection
- No response is sent to the client
- Conserves bandwidth
- Prevents information leakage