Skip to content
Vulnerabilities

The Host header is user input

A password reset link that points at somebody else server. A cached page served to every visitor with an attacker script in it. A rate limit that counts nothing. All three come from the same habit: reading a request header as though it described your infrastructure rather than something the caller typed.

8 min read

Key takeaways

  • Every header arrives with the request, so every header is attacker-controlled unless a proxy you operate overwrites it.
  • The Host header is the usual entry point because frameworks expose it as though it were configuration.
  • Password reset poisoning is the highest impact version: your system emails the victim a link pointing at the attacker.
  • The same value cached becomes everybody problem rather than one request problem.
  • The fix is an allowlist of hostnames you serve and a configured base URL, not sanitisation.

The habit behind all of it

When your application needs to build an absolute URL, the nearest value is usually the host from the incoming request. Frameworks make it easy and it reads like a fact about where the application is running.

It is not a fact. It is a line the caller sent, and they can send anything. The same is true of forwarding headers, the referring page, the client user agent, and any custom header your infrastructure adds, unless something you control replaces it on the way in.

The rule

A header is environment only if a component you operate sets it and overwrites whatever the caller supplied. If your proxy appends rather than replaces, the left-most value - the one everything downstream reads as the client - is chosen by the caller.

Password reset poisoning

The highest impact form, because it turns your own email into the delivery mechanism.

An attacker requests a reset for a victim address, supplying a Host header pointing at a server they control. The application generates a valid token and builds the link using that host. The victim receives a genuine email, from your domain, correctly signed, containing a link that sends the token somewhere else. If they click it, the attacker has a working reset token.

Nothing about the message looks wrong. It came from you because it did come from you, and that is what makes this worse than an ordinary phishing attempt.

When a cache is in front

On its own, a reflected header affects one request. Behind a cache, it can be stored and served to everybody who asks for the same page.

The mechanics are unglamorous: the cache decides what counts as the same request using a key, usually the method and the path. If a header outside that key changes the response, an attacker can send one request that poisons the entry and let the cache distribute it.

  • A header reflected into the page body or into an absolute asset URL.
  • A response that varies on a header the cache does not include in its key.
  • Redirects built from the request host and then cached.
  • Error pages that echo request detail and are cacheable.

The other headers people trust

Commonly trusted headers and what goes wrong
HeaderTrusted forWhat breaks
HostBuilding absolute URLs and routing.Reset poisoning, redirects to attacker infrastructure, cache poisoning.
X-Forwarded-ForRate limiting, logging, geographic rules.Spoofable unless the proxy replaces it. A limit keyed on it counts nothing.
X-Forwarded-Host and X-Forwarded-ProtoReconstructing the original request behind a proxy.The same problems as Host, in a header people forget to allowlist.
RefererAccess decisions and cross-site request checks.Attacker controlled and often absent, so any rule built on it fails in both directions.
Custom internal headersMarking a request as internal or already authenticated.If it reaches the application from outside, anybody can set it.

The last row is the one that turns into a full authentication bypass. A header meaning "this request came from inside" is a password written in the clear, and it works for anyone who guesses the name.

Fixing it

  1. Configure the hostnames you serve and reject anything else at the edge, before the application sees it.
  2. Build absolute URLs from configuration, never from the request. Email links especially.
  3. Have the proxy replace forwarding headers rather than appending to them, so the value downstream is one you set.
  4. Strip inbound copies of any header your infrastructure adds, so an outside caller cannot supply their own.
  5. Include in the cache key any header that can change the response, or make the response not vary on it.
  6. Never treat a header as proof of origin or authentication.

Sanitising the value is the wrong instinct. You are not trying to make an arbitrary hostname safe, you are trying to establish that it is one of yours, and a fixed list is the only thing that does that.

Testing for it

  1. Send a request with a Host header naming a domain you control and see whether the response reflects it or redirects there.
  2. Trigger a password reset with the same substitution and read the link in the email that arrives.
  3. Repeat with the forwarded-host and forwarded-proto variants, which are often missed by a Host allowlist.
  4. If a cache sits in front, check whether a reflected value persists into a subsequent clean request.
  5. For rate limiting, send the same requests with varying forwarded-for values and see whether the limit still applies.

The reset test is the one to run first. It is quick, it is unambiguous, and a positive result is an account takeover chain rather than a curiosity.

FAQ

Questions this raises

What is host header injection?

Supplying a Host header the application then trusts, usually to build an absolute URL. Because it arrives with the request it is attacker-controlled, so the URLs your application generates can be made to point somewhere else.

What is password reset poisoning?

The highest impact form. An attacker requests a reset for somebody else while supplying a host they control, so the genuine email your system sends contains a link that delivers the reset token to the attacker.

Can we not just sanitise the header?

Sanitising answers the wrong question. You do not need the value to be harmless, you need to know it is one of yours, and only a fixed list of hostnames checked at the edge establishes that.

Is X-Forwarded-For safe to rate limit on?

Only if a proxy you operate replaces it rather than appending to it. If it appends, the left-most entry is chosen by the caller, and a limit keyed on it can be reset by changing one header.

How does a cache make this worse?

Alone, a reflected header affects the one request that carried it. Behind a cache that does not include the header in its key, a single request can store a poisoned response and the cache serves it to everybody who asks for the same page.