Host Header Poisoning
Also known as: Host header injection, X-Forwarded-Host poisoning, Password reset poisoning, Web cache poisoning via Host
A web flaw in which an application trusts the Host or X-Forwarded-Host request header to build absolute URLs or cache keys, so a caller-supplied value can redirect reset links to another domain or poison cached pages.
How it works
Host header poisoning happens when an application treats the Host header, or a forwarding header such as X-Forwarded-Host, as trustworthy and uses it to build absolute URLs or to decide how a response is cached. The header is supplied by whoever sends the request, so the value can name any domain, not just the site the server actually runs.
The classic impact is on password recovery. If the reset email is built as scheme plus request Host plus a token path, a request carrying an unexpected Host produces an email whose link points at another domain, and the token can leak when the recipient follows it. A second impact is cache poisoning: if a shared cache stores a page generated with a foreign Host but keys it without that header, later visitors receive links or scripts pointing off-domain.
The defence is to stop deriving the application's identity from the request. Build links from a configured canonical base URL, reject requests whose Host is not on an allowlist at the edge, honour X-Forwarded-* headers only when they arrive from a trusted proxy, and make sure cache keys include every input that changes the response.
Detection is mostly a matter of looking for the pattern: absolute URLs assembled from request headers in code review, and requests in logs whose Host or X-Forwarded-Host does not match any domain you own, especially on recovery and redirect endpoints.
Walk through it
- 1Review the link builder
- 2Name the untrusted input
- 3Pick the trusted source
- Allowlist Host at the edge
- Cache keys, proxies and verification
A review flags the password-reset email. Read how the application decides which address to put in the link, and note where that value comes from.
1// Builds the link emailed to the user2export function resetLink(req, token: string) {3 const host = req.headers["x-forwarded-host"] ?? req.headers.host;4 return `https://${host}/reset?token=${token}`;5}6 7// What it should do: use configuration the server owns8// return `${config.publicBaseUrl}/reset?token=${token}`;Spot it
- Requests whose Host or X-Forwarded-Host does not match any domain the organisation owns.
- Password-reset or invitation emails containing links to a domain other than the application's own.
- Recovery, redirect and email-sending endpoints receiving a Host different from the one on the TLS certificate.
- Cached pages that contain absolute URLs pointing off-domain.
- Code that builds absolute URLs from request headers, found in review or static analysis.
Edge / reverse proxy access log
ts=2026-10-11T08:02:11Z method=POST path=/account/forgot-password host=unexpected-domain.example xfh=- status=200 upstream=app-2
ts=2026-10-11T08:02:48Z method=POST path=/account/forgot-password host=account.contoso-retail.example xfh=- status=200 upstream=app-1Mail gateway
ts=2026-10-11T08:02:12Z template=password_reset to=u_7710 link_domain=unexpected-domain.example allowed_domain=falsesplSplunk: requests with an unexpected Host header
index=edge sourcetype=access
| where NOT match(host, "^(account|www)\.contoso-retail\.example$")
| stats count by host, xfh, path, src_ipHealth checks and internal probes may use IP hosts; allow-list them before alerting.
grepgrep: recovery requests with a foreign Host
grep 'path=/account/forgot-password' access.log | grep -v 'host=account.contoso-retail.example'Stop it
Build absolute URLs from a configured canonical base URL
Keep the application's public address in server configuration and use it for reset links, redirects, emails and feed URLs. Never derive it from the Host or X-Forwarded-Host of the current request.
Allowlist expected Host values and reject the rest at the edge
Configure the proxy, load balancer or framework to accept only the hostnames you serve and return an error for anything else, so unexpected values never reach application code.
Trust X-Forwarded-* only from your own proxy, and key caches correctly
Honour forwarding headers only when the connection comes from a known proxy, strip them from other sources, and make sure any shared cache keys on every header that changes the response.
Use a configured base URL, not the request Host
Vulnerable
export function resetLink(req, token: string) {
const host = req.headers["x-forwarded-host"] ?? req.headers.host;
return `https://${host}/reset?token=${token}`;
}Hardened
export function resetLink(token: string) {
return `${config.publicBaseUrl}/reset?token=${encodeURIComponent(token)}`;
}The function no longer takes the request at all, so no header can influence the link.
Edge allowlist of accepted hostnames (illustrative)
Hardened
allowed_hosts:
, account.contoso-retail.example
, www.contoso-retail.example
on_unlisted_host: reject_with_421
trust_forwarded_headers_from:
, 10.0.0.0/24 # our load balancers only- Store the public base URL in configuration and use it everywhere an absolute URL is built.
- Enforce a Host allowlist at the CDN, proxy or framework and reject unknown hosts.
- Accept X-Forwarded-Host and similar headers only from trusted proxies; drop them otherwise.
- Include all response-affecting inputs in shared cache keys, or do not cache pages that embed absolute URLs.
- Review email, redirect and feed code for any URL built from request headers.
- Add a regression test that sends a foreign Host and asserts the output still uses the canonical domain.
If it already happened
Deploy the allowlist at the edge or switch link generation to the configured base URL, and purge any cache entries that contain off-domain URLs.
Find emails sent with off-domain links, invalidate the reset tokens they carried, and remove every code path that builds URLs from request headers.
Notify affected users, force a reset where tokens may have leaked, and confirm with a test that a foreign Host yields only canonical links.
Add a review checklist item for URL construction, alert on unexpected Host values, and put the canonical base URL in the framework template.
Check yourself
1. Why is it unsafe to build a password-reset link from the request's Host header?
2. What is the primary fix for code that builds absolute URLs from request headers?
3. When should an application honour X-Forwarded-Host?