Host Header Injection: Reset Poisoning & Cache Attacks
How attackers abuse the HTTP Host header for password-reset poisoning, cache issues, and routing tricks, why it happens, and how to defend against it.
Every HTTP request carries a Host header naming the site the client wants. It exists so one server can host many domains and route each request to the right one. That is useful and mundane. The problem is that the client sets this header, and a great deal of application code reads it back as though the application had chosen it.
Host header injection is what happens when that misplaced trust becomes an attack surface. An application that builds links, makes routing decisions, or generates cached responses from the incoming Host header is taking direction from whoever sent the request. In the worst cases, that direction leads to account takeover.
Why the Host header is trusted at all
The header is genuinely load-bearing. Virtual hosting depends on it: a single IP address may serve dozens of sites, and the server uses the Host header to pick which one. Reverse proxies and load balancers pass it along, sometimes rewriting it, sometimes adding companions like X-Forwarded-Host. Application frameworks expose it through convenient helpers that return "the current host", and developers reach for those helpers to build absolute URLs.
That convenience is the trap. A helper that claims to return the current host is really returning the client's claim about the current host. When an email template, a redirect, or a script tag is built from that value, the attacker gets to influence the output.
It is worth separating the two ways the header is used, because only one is dangerous. Using the Host header to route a request to the correct virtual host is fine, since the server is only deciding which site to serve and a wrong value simply lands on the wrong site or an error. The danger begins when the value is fed back into content or logic: into a link, a redirect, a cached response, or an internal request. At that point the server has taken an attacker's input and turned it into part of its own behavior. Keeping those two uses distinct is the first step to reasoning about the risk.
Password reset poisoning
The clearest and most damaging case is password-reset poisoning. Self-service password reset usually works like this: the user submits their email, the app generates a one-time reset token, and it emails a link containing that token. To build the link, the app needs its own domain name, and too often it reads that domain from the Host header of the reset request.
An attacker who knows a victim's email address submits the reset form on the victim's behalf, but sends a Host header pointing at a server the attacker controls. The application generates a valid reset token for the victim and emails a link like this to the victim's real inbox:
https://attacker-controlled.example/reset?token=VALID_TOKEN_FOR_VICTIM
If the victim clicks, their browser sends the live token to the attacker's server. The attacker replays it against the real site and resets the password. The token was legitimate. The domain in the link was not. On MITRE ATT&CK this kind of internet-facing exploitation maps to Exploit Public-Facing Application (T1190).
Some variants do not even need the victim to click. If the app leaks the token elsewhere, or if the reset link is fetched by a preview bot, the token can reach the attacker's server without a human in the loop.
A password-reset URL must be constructed from a hardcoded or configured base URL that the application owns. The moment the domain in that link comes from the Host header, X-Forwarded-Host, or any other request-controlled value, self-service reset becomes an account-takeover primitive. This single rule closes the most severe Host header bug.
Cache poisoning and other impacts
Password reset is the headline, and it is not the only outcome. The same trust surfaces in several places.
| Technique | Mechanism | Typical impact |
|---|---|---|
| Password-reset poisoning | Reset link domain read from Host header | Account takeover |
| Web cache poisoning | Host-derived content cached and served to others | Stored redirect or script for many users |
| Cache key confusion | Host header excluded from the cache key | One user's poisoned response reaches everyone |
| Routing and SSRF-style tricks | Override headers reach an internal virtual host | Access to unintended backends |
| Password-reset without click | Token leaks to Host-derived resource loads | Silent takeover |
Web cache poisoning is worth singling out. If the application reflects the Host header into a response, and the cache in front of it stores that response without including the Host in the cache key, the attacker can poison a shared cache entry. A single crafted request plants a malicious redirect or script that the cache then serves to every subsequent visitor. The details of that chain live in Web Cache Poisoning Explained. A Host-derived redirect that lands users on an attacker domain is also a flavor of the problem covered in Open Redirect Explained.
The override-header trap
Teams that learn about this bug often check the Host header and stop there. Attackers then reach for the forwarding headers. Proxies and frameworks frequently honor X-Forwarded-Host, X-Host, X-Forwarded-Server, or a rewritten Host, and application code may prefer those values when present. Duplicate Host headers and absolute request lines are further ways to smuggle a second, attacker-chosen host past a naive check.
The lesson is that validating one header is not enough while the framework silently consults others. You have to know exactly which value your code trusts and control all the inputs that can feed it. Overlapping header parsing between a proxy and an app server is also the same soil that grows the desync problems in HTTP Request Smuggling Explained.
How to find it
Testing for Host header injection is mostly a matter of changing the header and watching where the value surfaces. A few checks find most instances.
Start with the reflection test. Send a request with a Host header set to an obviously wrong value and look for that value coming back anywhere: in the HTML body, in a Location redirect header, in a canonical link tag, or in an absolute URL inside the response. Reflection is a strong signal that request-controlled host data reaches the output.
Move on to the reset flow, which is where the real damage lives. Trigger a password reset for an account you control while sending a modified Host header, then read the email that arrives. If the reset link points at the host you supplied rather than the real domain, you have confirmed the account-takeover primitive. Repeat the test with the forwarding headers, X-Forwarded-Host and its relatives, because an app that validates the Host header directly may still trust an override header.
Finally, probe routing. Try an absolute URL in the request line, duplicate Host headers, and unusual host values to see whether the server routes you to an unexpected virtual host or a default backend it should not expose. Any of these behaviors marks a place where the application is taking direction from a value it should treat as untrusted input.
How to actually defend against Host header injection
The defenses come down to one principle: your application should decide its own identity, not learn it from the request.
- Build absolute URLs from configuration. Set a canonical base URL in server-side config and use it for every generated link, especially password-reset and email links. Never assemble these from a request header.
- Allowlist valid hosts. Maintain an explicit list of the domains your application serves and reject any request whose effective host is not on it. Return an error or a default site rather than processing an unknown host.
- Neutralize forwarding headers. Decide at the edge which forwarding headers you trust, strip the rest at the proxy, and make sure application code cannot fall back to an override header you did not sanction.
- Keep the Host in the cache key. Ensure any cache in front of the app includes the host in its key and does not store responses that reflect an unvalidated host. This prevents one poisoned response from spreading.
- Do not reflect the Host header into responses. Avoid echoing the header into HTML, headers, or links. If a canonical link is needed, use the configured base URL.
- Pin virtual-host routing. Configure the web server to serve a default or reject unknown hosts, so requests for unexpected hostnames cannot reach sensitive internal virtual hosts.
For any app, ask two things. First, where does a generated link get its domain? If the answer is anything request-derived, fix it. Second, what exact value does the framework return for the current host, and which headers can influence it? Control every input to that value and the class of bug closes.
Host header injection is a trust bug wearing an infrastructure costume. The header looks like plumbing, so it gets waved through, and then application logic leans on it for identity. Give your application a fixed sense of its own hostname, validate incoming hosts against an allowlist, and keep the Host out of your caches and your emails, and the attacker loses the lever entirely. To see how public-facing exploitation like this trends over time, cross-reference our Exploit Intelligence dashboard.
A worked walkthrough of reset poisoning
Tracing the reset-poisoning flow end to end shows how a routine feature becomes account takeover, without needing any invented incident. Consider an application with standard self-service password reset. A user enters their email, the server generates a single-use reset token tied to that account, and it sends an email containing a link the user clicks to set a new password.
The flaw sits in how the server builds that link. It needs its own domain to form an absolute URL, and it reaches for a framework helper that returns the current host. That helper returns the value from the request's Host header, which the client set. On a normal request from a normal browser, the Host header names the real site, so the link looks correct and nobody notices where the domain came from.
The attacker changes one thing. They know the victim's email address, which is often public. They submit the reset form on the victim's behalf, and in that request they set the Host header to a domain they own. The server does exactly what it always does: it generates a valid, single-use reset token for the victim's real account, builds the link using the host from the request, and emails it to the victim's real inbox. The email is genuine, sent by the real service, and it lands in the victim's mailbox where it is trusted.
The link inside it points at the attacker's domain and carries the victim's live token as a parameter. If the victim clicks, their browser sends that token to the attacker's server, where the attacker reads it from the request log. The attacker then visits the real site, supplies the token, and sets a new password on the victim's account. The token was legitimate the entire time. The only thing the attacker controlled was the domain in the link, and they controlled it because the server asked the request what its own name was. This kind of internet-facing exploitation maps to MITRE ATT&CK Exploit Public-Facing Application, and the underlying weakness aligns with CWE-20, Improper Input Validation, applied to a header the application should never have trusted.
Some variants skip the click. If a mail gateway or a link-preview bot fetches the URL to render a preview, the token can reach the attacker's server with no human action at all, which turns a phishing-shaped attack into a silent one.
Detection signals
Host header injection is testable by observation, and it is also detectable in production once you know what to watch. The signals split between request-side anomalies and content-side reflection.
On the request side, log the effective host your application resolves for each request and compare it against your allowlist of real domains. Any request whose host is a domain you do not own is either a misrouted request or a probe, and a cluster of them against sensitive endpoints like password reset is a strong signal. Pay equal attention to the forwarding headers. Requests that carry X-Forwarded-Host, X-Host, or X-Forwarded-Server with a value different from the real Host, duplicate Host headers in a single request, or an absolute URL in the request line are all shapes an attacker uses to smuggle a second host past a naive check.
On the content side, the tell is reflection. Watch for responses where an inbound host value appears in a Location redirect header, a canonical link tag, an absolute URL in the body, or an email your system generates. The highest-value monitor sits on the reset flow specifically: if the domain in an outbound reset email ever differs from your canonical base URL, something is deriving that link from the request, and that is the exact bug.
| Signal | Where to look | What it suggests |
|---|---|---|
| Effective host outside your allowlist | Application request logs | A probe or misrouted request |
X-Forwarded-Host differing from Host | Proxy and app logs | Override-header smuggling |
| Duplicate Host headers or absolute request line | Raw request capture | An attempt to bypass a single-header check |
Inbound host reflected in Location or body | Response inspection | Request-derived host reaching output |
| Reset email domain differs from canonical | Outbound mail monitoring | Active reset poisoning |
The clearest possible detection for the worst variant is watching what leaves your system. Assert that every password-reset and account-notification link uses your configured canonical domain. A single email whose link points elsewhere is a live account-takeover attempt in progress, and it is trivial to alert on because the correct value is a constant you control.
How Host header injection compares to related bugs
Several web bugs share the theme of a request-controlled value steering server behavior. Placing them side by side clarifies what is specific to the Host header.
| Technique | Attacker-controlled input | Primary impact | Core fix |
|---|---|---|---|
| Host header injection | The Host or a forwarding header | Reset poisoning, cache poisoning | Derive identity from configuration |
| Open redirect | A redirect target parameter | Sending users to an attacker site | Allowlist redirect destinations |
| Web cache poisoning | Any unkeyed input reflected in a response | Poisoned response served to many | Key the cache on all reflected inputs |
| Request smuggling | Ambiguous framing between proxy and server | Requests routed or split unexpectedly | Consistent request parsing |
The connective tissue is that each treats an untrusted request value as authoritative for something it should not control. Host header injection is distinctive because the value it corrupts is the application's own identity, which then flows into links, emails, and cache entries. That is why the fix refuses to derive identity from the request at all rather than sanitizing the value.
Common misconceptions
"Using the Host header for routing is dangerous." Routing to the correct virtual host is a safe use, because the worst outcome of a wrong value is landing on the wrong site or an error page. The danger begins only when the header feeds content or logic: a link, a redirect, a cached response, or an internal request. Keeping those two uses separate is the whole reasoning.
"We validate the Host header, so we are covered." Validating the Host header alone leaves the override headers open. Frameworks and proxies frequently prefer X-Forwarded-Host when it is present, so an app that checks Host and ignores the forwarding headers is still exploitable. You have to know the exact value your code trusts and control every input that can feed it.
"Only the login and reset pages matter." Any generated absolute URL is a candidate, including email notifications, share links, webhooks that call back a URL, and canonical tags that affect how search engines and caches treat your pages. The audit question applies everywhere a link is built, across the whole application.
"HTTPS prevents this." Transport encryption protects the header in transit and does nothing about the fact that the client set it. The Host header is attacker-controlled input regardless of whether the connection is encrypted, so HTTPS is unrelated to this class of bug.
How the problem persists
The Host header dates to the introduction of name-based virtual hosting, which let one server address serve many sites by reading the requested host from the request itself. That design is sound for routing, and it quietly created the assumption that the header names the site truthfully. Application frameworks then wrapped it in convenient helpers labeled as the current host, and developers reasonably used those helpers to build absolute URLs.
The gap widened as reverse proxies and load balancers became standard. Those layers pass the host along and often add forwarding headers, so an application might read the original Host, a rewritten one, or an override header depending on configuration, and few teams map exactly which value their code ends up trusting. That ambiguity is what keeps the bug alive: the same helper can return a safe value in one deployment and an attacker-controlled one in another, purely because of proxy configuration.
The defensive consensus that emerged is straightforward and stable. The application should decide its own identity from server-side configuration, validate incoming hosts against an allowlist, and keep the host out of caches and generated links. Teams that adopt that principle stop chasing individual reflections and close the class.
Frequently asked questions
Is the Host header ever safe to use? Yes, for choosing which virtual host serves a request. That use only decides which site to serve, and a wrong value lands on the wrong site or an error. The header becomes unsafe when its value flows into content or logic, such as a generated link, a redirect, a cached response, or an internal request.
Why does sanitizing the Host header not fix it? Sanitizing is fragile because the framework may consult several headers and you have to control all of them, and because the safe value is something you already know: your own domain. Building links from a configured canonical base URL removes the need to trust or clean any request header.
Do I need to worry about this behind a reverse proxy?
Especially so. Proxies add forwarding headers that application code may prefer over the Host header, so a check on Host alone can be bypassed through X-Forwarded-Host and its relatives. Decide at the edge which forwarding headers you trust, strip the rest, and make sure the app cannot fall back to one you did not sanction.
How is this different from an open redirect? An open redirect sends a user to an attacker destination through a redirect parameter. Host header injection corrupts the application's own sense of its domain, which then poisons links, emails, and cache entries. A Host-derived redirect is one overlap between the two, and the root causes differ.
What is the single most important fix? Build every absolute URL, above all password-reset and email links, from a canonical base URL set in server-side configuration. That one rule closes the most severe outcome, reset poisoning, regardless of what any request header claims.
How do I keep a shared cache from spreading a poisoned response? Ensure the cache includes the host in its cache key and does not store responses that reflect an unvalidated host. If the host is part of the key, a response crafted for an attacker-supplied host cannot be served back to visitors requesting the real host.
Related guides
Sources & further reading
Related guides
- HTTP Request Smuggling: Front-End/Back-End Desync
How HTTP request smuggling exploits parsing disagreements between front-end and back-end servers, the CL.TE and TE.CL variants, and how to defend.
- Web Cache Poisoning: Cache Keys, Unkeyed Inputs & Defense
How web cache poisoning turns a shared cache into a delivery system for attacker content by abusing unkeyed inputs, and how to defend the cache key.
- Clickjacking Explained: UI Redress Attacks & Frame Defenses
How clickjacking hijacks a user's clicks with transparent iframes, what an attacker can trigger, and how to block framing with headers and CSP.