Host header injection: how it works, how to detect it, how to fix it
The app trusts a client-supplied Host or X-Forwarded-Host value when building links, enabling reset poisoning and cache attacks.
Typical severity: High · Checked by NovaPentest module: Host Header Injection
What it is
Host header injection covers vulnerabilities where an application treats the Host header, or a forwarding header that stands in for it, as trusted when building links, redirects or routing decisions. Both values come from the client and can be set to anything.
In HTTP/1.1 the Host header tells the server which site a request is for, which lets one IP address serve many sites. When a request passes through a reverse proxy or load balancer, the original host is often passed along in headers such as X-Forwarded-Host, X-Host or X-Forwarded-Server. Many frameworks resolve "the current host" from these headers automatically so that generated URLs match the public domain.
The problem appears when that value reaches something that matters: a password reset link in an email, an absolute URL in a Location header, a script or canonical URL in the page, a cache entry, or the choice of an internal virtual host. If the application accepts any value instead of checking it against the domains it actually serves, an attacker controls part of that output.
Web servers often route an unknown Host value to a default virtual host, which may be the application itself, so the web server alone doesn't prevent this. Forwarding headers are a frequent blind spot: they are only supposed to be set by your own proxies, but unless the application is configured to know which proxies to trust, it can't tell a proxy-set header from one sent by the client.
The OWASP Web Security Testing Guide covers this class under "Testing for Host Header Injection".
How it is exploited
How it is exploited depends on where the injected host ends up.
1. Password reset poisoning. This is the most common high-impact case. The attacker submits a password reset for the victim's email address with a forged Host or X-Forwarded-Host pointing at a domain they control. The application builds the reset link from that value and emails it to the victim. If the victim clicks it, the reset token goes to the attacker's server, and the attacker uses it on the real site to take over the account. The email genuinely comes from the real service, which makes the link look trustworthy.
2. Web cache poisoning. If a cache doesn't include the forwarding header in its key, one request with a forged host can store a response containing attacker-controlled links or script URLs and serve it to every visitor.
3. Redirects through Location. An absolute redirect built from the header points off-site when the value is forged. Browsers won't send a forged header on a victim's behalf, so this usually matters in combination with caching.
4. Internal virtual hosts and routing. Setting Host to an internal name, such as an admin virtual host, can reach applications meant to be internal only, or skip access rules keyed on the host.
5. Server-side request manipulation, where the host value is used to build calls to back-end services.
A reflection that stays in the attacker's own response is harmless on its own, since a victim's browser won't send the forged header. The impact comes from reflections that persist or reach someone else: emails, caches and server-side routing. That is why testing the actual reset flow matters.
How to detect it
NovaPentest's host header check sends GET requests to the target URL, without following redirects, with a forged host value in one proxy forwarding header at a time: X-Forwarded-Host, then X-Host, then X-Forwarded-Server. The real Host header is not changed. The forged value is a test domain that belongs to the scanner, not to the target.
If the forged domain appears in the response body or in the Location header, it reports "Host header injection" at High severity, naming the header and whether the reflection was in Location or in the body, mapped to CWE-644 and OWASP A01:2021. It stops after the first header that reflects.
Limits worth knowing: the check doesn't send a modified Host header, doesn't try absolute-URI or duplicate-Host variations, and doesn't exercise password reset or other email flows. It confirms the application trusts a forwarding header in the response it generates, not that a specific attack such as reset poisoning works. It only tests the configured target URL, and other endpoints, including reset pages, may behave differently. The target URL is rebuilt from scheme, host and path, so a non-default port in the target is not kept. X-Forwarded-Host and X-Host are also tested by the cache poisoning check, so one root cause can produce both findings.
To verify manually:
1. Reproduce the reflection: curl -s -D - -H "X-Forwarded-Host: test.invalid" https://example.com/ | grep -i test.invalid, which searches both the headers and the body.
2. Test the Host header itself: curl -s -D - -H "Host: test.invalid" https://example.com/ and see whether you get the application or an error.
3. With a test account, request a password reset while sending the forged header, and inspect the link in the email you receive.
How to fix it
1. Never build security-relevant URLs from request headers. Password reset links, email verification links, OAuth redirect URIs and absolute redirects should use a public base URL from configuration, such as https://app.example.com. This single change stops reset poisoning whatever headers arrive, so do it first.
2. Validate Host against an allowlist. Accept only the domains the application serves and reject the rest with a 400. In ASP.NET Core, set AllowedHosts to your real domains instead of a wildcard; in Django, set ALLOWED_HOSTS. At the web server, make the default virtual host a catch-all that refuses the request instead of serving the application, for example an nginx server block with listen 80 default_server; and return 444;.
3. Trust forwarding headers only from your own proxies. Accept X-Forwarded-Host and similar headers only from known proxy addresses: KnownProxies or KnownNetworks in ASP.NET Core's ForwardedHeadersOptions, a specific address list for "trust proxy" in Express, and USE_X_FORWARDED_HOST in Django only when required. Have the edge proxy overwrite these headers with the real value rather than passing through what the client sent.
4. Strip headers you don't use. If nothing needs X-Host or X-Forwarded-Server, drop them at the edge.
5. Make caches key on any header the application uses to build responses, or don't cache those responses.
6. Keep internal-only virtual hosts on separate listeners or networks, not merely behind a different Host value.
After deploying, retest each forwarding header and the Host header, and check the link in a real reset email.
Check your own site
Start with the free passive scan: no account needed, and you get an A to F grade covering headers, TLS, cookies, DNS and other configuration a visitor can observe. It does not send attack payloads, so it will not confirm every class of flaw. The full scan, available with a free account, runs the active checks, including the one described on this page.