Host Header Injection: How Trusted Headers Become Attack Vectors
The HTTP Host header tells a web server which website the client wants to reach. In the early days of the web, this was a simple routing mechanism for virtual hosting — one server, multiple websites. Today, applications rely on the Host header for far more than routing. They use it to generate absolute URLs, construct email links, determine redirect destinations, and create cache keys. The problem: the Host header comes from the client and is trivially manipulated.
Host header injection is not a single vulnerability but a class of attacks that exploit an application's trust in this user-controlled value. The consequences vary from account takeover through password reset poisoning to widespread content manipulation through cache poisoning.
How the Host Header Works
Every HTTP/1.1 request includes a Host header:
GET /reset-password?token=abc123 HTTP/1.1
Host: example.com
The server uses this header to determine which virtual host should handle the request. But many applications also use it programmatically:
# Common pattern — using the Host header to build URLs
reset_link = f"https://{request.headers['Host']}/reset?token={token}"
send_email(user.email, f"Reset your password: {reset_link}")An attacker can set the Host header to any value:
GET /forgot-password HTTP/1.1
Host: evil.com
If the application uses the Host header to construct the reset link, the victim receives an email with a link pointing to https://evil.com/reset?token=abc123.
Attack Vectors
Password Reset Poisoning
This is the most impactful and most commonly exploited host header injection attack. The attack flow:
- The attacker identifies that the application uses the Host header when generating password reset links
- The attacker submits a password reset request for the victim's email address
- The attacker modifies the Host header to point to a server they control
- The application generates a reset link using the attacker's Host value and sends it to the victim's email
- The victim receives a legitimate email from the real application (the email itself is genuine — only the link is modified)
- When the victim clicks the link, they are directed to the attacker's server, which captures the reset token
- The attacker uses the captured token on the real application to reset the victim's password
The attack is particularly effective because the email is genuinely sent by the application. Email security checks (SPF, DKIM, DMARC) all pass. The email content looks legitimate. Only the URL domain is different, and many users do not inspect link destinations before clicking.
In a security assessment of a SaaS platform with over 50,000 users, testers found that the password reset feature used the Host header verbatim. The reset email template included:
Click here to reset your password:
https://{Host}/auth/reset?token={token}&email={email}
By submitting reset requests with a modified Host header, the testers demonstrated that any user account could be taken over. The platform had no rate limiting on reset requests, meaning an attacker could systematically target every user.
Web Cache Poisoning
When a caching layer (CDN, reverse proxy cache, or application cache) sits in front of an application, host header injection can poison the cache and serve malicious content to every user.
The attack relies on a mismatch between the cache key and the response content:
- Cache key: The cache typically keys responses on the URL path and query string
- Response content: The server generates the response using the Host header, which is not part of the cache key
- The mismatch: An attacker requests a page with a manipulated Host header. The server generates a response with malicious URLs. The cache stores this response keyed to the URL path. Every subsequent user requesting the same path receives the poisoned response.
GET / HTTP/1.1
Host: evil.com
Response:
<html>
<head>
<link rel="stylesheet" href="https://evil.com/styles.css">
<script src="https://evil.com/app.js"></script>
</head>
...
This response is cached. Every user who visits the homepage now loads CSS and JavaScript from the attacker's server. The attacker has achieved persistent cross-site scripting through the cache, without exploiting any XSS vulnerability in the application code.
During an assessment of a media company's website, testers discovered that the application generated absolute URLs for static assets using the Host header, and the CDN cached responses without including the Host header in the cache key. A single poisoned request caused the homepage to load an attacker-controlled JavaScript file for all visitors until the cache expired — a thirty-minute window affecting hundreds of thousands of users.
Server-Side Redirect Manipulation
Applications frequently use the Host header to construct redirect URLs:
# After login, redirect to the dashboard
return redirect(f"https://{request.headers['Host']}/dashboard")An attacker who can manipulate the Host header can redirect users to a phishing site after authentication. The victim enters their credentials on the legitimate login page, and the POST request is processed correctly, but the subsequent redirect sends them to the attacker's site.
This is particularly dangerous in OAuth flows, where redirect URIs are critical security parameters. If the application derives the redirect URI from the Host header, the entire OAuth token exchange can be hijacked.
Internal Header Injection
Some applications and proxies use additional headers alongside Host:
- X-Forwarded-Host — Set by reverse proxies to indicate the original Host header
- X-Host — Non-standard header used by some frameworks
- X-Forwarded-Server — Another proxy header
Applications may check the Host header but trust these alternative headers without validation:
GET /forgot-password HTTP/1.1
Host: legitimate.com
X-Forwarded-Host: evil.com
The application sees Host: legitimate.com and allows the request, but its URL generation function reads X-Forwarded-Host first (as it should behind a proxy), resulting in URLs pointing to the attacker's domain.
Virtual Host Brute-Forcing
In shared hosting environments, an attacker can enumerate virtual hosts by sending requests with different Host header values and observing the responses. This reveals internal applications, staging environments, and admin panels that are hosted on the same server but intended to be accessed only through specific domain names.
GET / HTTP/1.1
Host: admin.internal.example.com
→ 200 OK (admin panel exposed)
GET / HTTP/1.1
Host: staging.example.com
→ 200 OK (staging environment with debug mode enabled)
Prevention Strategies
Use a Hardcoded Canonical Domain
The most reliable fix is simple: never use the Host header for URL generation. Configure your application with a canonical domain name in its settings:
# In application configuration
CANONICAL_DOMAIN = "https://example.com"
# In code — use the configured domain, not the request header
reset_link = f"{settings.CANONICAL_DOMAIN}/reset?token={token}"
send_email(user.email, f"Reset your password: {reset_link}")This eliminates the attack surface entirely. The Host header can contain anything — the application ignores it for URL generation.
Validate the Host Header
If your application must use the Host header (for example, to support multiple legitimate domains), validate it against an allowlist:
ALLOWED_HOSTS = {"example.com", "www.example.com", "app.example.com"}
def validate_host(request):
host = request.headers.get("Host", "").split(":")[0] # Strip port
if host not in ALLOWED_HOSTS:
return HttpResponse(status=400)Most web frameworks provide this functionality:
- Django: The
ALLOWED_HOSTSsetting rejects requests with unrecognized Host headers - Rails: The
config.hostssetting performs the same validation - Express/Node: Requires manual implementation or middleware
Configure the Reverse Proxy
Your reverse proxy or load balancer should overwrite the Host header before forwarding requests to the application:
Nginx:
server {
server_name example.com;
location / {
proxy_pass http://backend;
proxy_set_header Host example.com; # Override, don't pass through
proxy_set_header X-Forwarded-Host $host;
}
}Apache:
ProxyPreserveHost Off
ProxyPass / http://backend/
ProxyPassReverse / http://backend/
RequestHeader set Host "example.com"This ensures the backend application always receives the correct Host value regardless of what the client sends.
Ignore or Validate Forwarded Headers
If your application reads X-Forwarded-Host, X-Forwarded-Proto, or similar headers, ensure these are set only by your trusted proxy infrastructure:
- Strip these headers at the edge — Your outermost proxy should remove any
X-Forwarded-*headers sent by the client before adding its own - Configure trusted proxy addresses — Only accept forwarded headers from known proxy IP addresses
- Use a shared secret — Some proxy configurations support a secret header to verify the request passed through the proxy
Cache Key Configuration
If you use a CDN or caching proxy, ensure the Host header is included in the cache key:
# Vary header tells caches to key on Host
Vary: Host
Alternatively, configure your CDN to include the Host header in its cache key derivation. Most CDN providers offer this configuration but do not enable it by default.
Additional Protections
Absolute URL validation in emails: Even with a canonical domain, verify that all URLs in outgoing emails match the expected domain pattern before sending.
Same-origin checks for redirects: After authentication, validate that redirect targets are same-origin or on an allowlist of permitted domains.
Monitor for anomalous Host values: Log and alert on requests with unexpected Host headers. These may indicate active exploitation attempts.
Testing for Host Header Injection
Testing is straightforward and should be performed on every application:
- Modify the Host header in password reset, email verification, and invitation flows. Check if the generated links use the modified value.
- Send duplicate Host headers — some servers use the first, others use the last:
Host: evil.com Host: legitimate.com - Test X-Forwarded-Host and similar headers while keeping the Host header legitimate
- Add a port to the Host header —
Host: evil.com:legitimate.comsometimes bypasses basic validation - Test with an absolute URL in the request line:
Some servers prioritize the URL in the request line over the Host headerGET https://evil.com/ HTTP/1.1 Host: legitimate.com - Check cache behavior — Send requests with modified Host headers and verify whether the response is cached and served to other users
Key Takeaways
Host header injection exploits a fundamental design assumption: that the Host header reflects the actual server. In practice, the Host header is an attacker-controlled input that should be treated with the same suspicion as any other user input.
The defenses are straightforward:
- Use a hardcoded canonical domain for all URL generation — never derive it from request headers
- Validate the Host header against an allowlist of permitted values
- Configure reverse proxies to overwrite the Host header before forwarding to backends
- Strip client-supplied
X-Forwarded-*headers at the network edge - Include the Host header in cache keys to prevent cache poisoning
- Test all email-generating features, redirect flows, and URL construction for host header trust
The vulnerability is simple, the fix is simple, but the consequences of leaving it unaddressed — account takeover, cache poisoning, and phishing — are severe.
Need your application tested for host header injection? Get in touch.