What is Host Header Injection? How a Trusted Header Goes Wrong

What is Host Header Injection? How a Trusted Header Goes Wrong

Every HTTP request carries a Host header that names the site the client wants to reach. Host header injection happens when an application reads that header and trusts it as the truth about its own identity, then uses the attacker supplied value to build links, cache keys, or routing decisions. The header is client controlled, so trusting it hands part of the app’s behavior to whoever sends the request.

What the Host header is and why apps trust it

One IP address can serve many sites. The Host header is how the client tells the server which site it wants. A normal request to Acme Notes looks like this:

GET /dashboard HTTP/1.1
Host: app.acmenotes.example
Cookie: session=...

The web server uses Host to pick the right virtual host. So far so good. The trouble starts when application code reads the same header to decide what the site’s own address is, for example when generating an email link or an absolute URL. The value came from the client, and a client can write anything there.

GET /dashboard HTTP/1.1
Host: evil.example
Cookie: session=...

If Acme Notes echoes that host back into a link, a redirect, or an email, the attacker has steered the app to point at a domain they own.

The Host header is a request from the client, not a fact about the server. Code that treats it as the server’s own identity is trusting input it should never have trusted.

How a host header injection attack plays out

Password reset poisoning

This is the impact that turns a small bug into account takeover. Acme Notes builds its password reset email by reading the request host and gluing the reset token onto it:

# Vulnerable: the base URL comes from the request
reset_link = "https://" + request.host + "/reset?token=" + token
send_email(user.email, reset_link)

An attacker submits the reset form for a victim’s account but sends a tampered host:

POST /forgot-password HTTP/1.1
Host: evil.example
Content-Type: application/x-www-form-urlencoded

email=victim@acmenotes.example

The server mails the victim a real reset link, but pointed at the attacker’s domain:

https://evil.example/reset?token=Ab19f3...c204

If the victim clicks it, their browser sends the valid token to evil.example. The attacker reads it from their own server logs and resets the password. The email came from Acme Notes, the token is genuine, and the only forged part was one header.

Web cache poisoning

If a cache sits in front of Acme Notes and the host header is reflected into a cached response, an attacker can poison the entry. Suppose a page echoes the host into an absolute script tag:

<script src="https://app.acmenotes.example/static/app.js"></script>

An attacker sends a request with Host: evil.example. If the cache stores that response under the normal cache key, the next real visitor receives a page that loads script from the attacker’s domain. See our note on web cache deception for how cache behavior turns one bad response into many.

Routing to internal vhosts and SSRF like behavior

Some setups route by host name to internal services. A tampered host such as Host: admin.internal or Host: localhost can reach a virtual host that was never meant to face the public internet. When a back end fetches a URL it built from the host header, the request can be steered at internal addresses, which overlaps with server side request forgery. The shape is the same: a client controlled value decides where a server side action points.

X-Forwarded-Host and friends

Even apps that validate Host often trust the headers a proxy adds. X-Forwarded-Host, X-Host, X-Forwarded-Server, and Forwarded are all attacker controllable when they reach the app directly, and many frameworks prefer X-Forwarded-Host over Host when building URLs.

POST /forgot-password HTTP/1.1
Host: app.acmenotes.example
X-Forwarded-Host: evil.example

email=victim@acmenotes.example

Here the Host looks clean, but the reset link still ends up on evil.example because the framework read the forwarded header first.

How to detect host header injection

  • Send a tampered host and watch the response. Against an app you own, change Host to a value you control and look for it reflected in links, redirects (the Location header), canonical tags, or script sources.
  • Trigger a password reset and read the email. Submit the reset form with a tampered Host and again with X-Forwarded-Host. If the link in the email points at your value, the email path is vulnerable.
  • Test the forwarded headers separately. A clean Host result does not clear the app. Repeat each check with X-Forwarded-Host and X-Host set.
  • Check the default vhost. Send a request with an unknown host. If the server answers with the real app instead of rejecting it, host based routing is loose.

How to prevent host header injection

  • Validate the host against an allowlist. Compare the incoming Host to a fixed set of known domains and reject anything else with a 400 before the request reaches application logic.
  • Build links from a canonical base URL in config. Store the site’s real address as a setting, for example BASE_URL=https://app.acmenotes.example, and build every absolute URL and email link from that value. Never concatenate the request host into a link.
  • Do not trust forwarded headers blindly. Only honor X-Forwarded-Host when it comes from a proxy you control, and strip it at the edge otherwise. Configure your framework’s trusted host or allowed host list explicitly.
  • Set a strict default virtual host. Configure the web server so requests with an unknown host get rejected instead of falling through to the main app. This closes loose routing and internal vhost access at the front door.

For the wider pattern of trusting client supplied data, the injection and input category collects related bugs, and the web security glossary defines the terms used here.

Why this rewards understanding the app

You do not find host header injection by firing a fixed payload at a URL. You find it by understanding where the app turns the request host into a link, a cache key, or a route, and then testing whether it ever validates that value. The bug is an assumption, that the host header tells the truth about the server, and the way to surface it is to test that assumption directly. That is the kind of bug an autonomous researcher built to test an app’s assumptions is meant to catch. You can read more about that approach on our about page.

Frequently asked questions

What is host header injection?

It is a bug where an application reads the client supplied Host header and trusts it as the truth about its own address, then uses that value to build links, cache keys, or routing decisions. Because any client can set Host to whatever it wants, an attacker can steer the app to point at a domain they control. The same risk applies to forwarded headers like X-Forwarded-Host.

How does password reset poisoning work?

An app that builds its reset link from the request host, such as "https://" + request.host + "/reset?token=" + token, can be tricked. The attacker submits the victim’s email in the reset form but sends a tampered Host: evil.example. The app mails the victim a genuine reset token pointed at the attacker’s domain. If the victim clicks, the valid token lands in the attacker’s logs and the account is taken over.

Is X-Forwarded-Host dangerous too?

Yes. Many frameworks prefer X-Forwarded-Host over Host when generating URLs, so an app that validates Host can still be exploited through the forwarded header. The same goes for X-Host, X-Forwarded-Server, and Forwarded. Only honor these headers when they come from a proxy you control, and strip them at the edge otherwise.

How do you prevent host header injection?

Validate the incoming Host against an allowlist of known domains and reject anything else with a 400. Build every absolute URL and email link from a canonical base URL stored in config, never from the request host. Configure your framework’s trusted host list explicitly, and set the web server’s default virtual host to reject requests with an unknown host instead of serving the main app.