X-Forwarded-Host Bugs That Behave Like SSRF
Classic SSRF means the server connects somewhere attacker-chosen. Host-header attacks often stop one step earlier: the application thinks it lives on your domain and writes that belief into emails, redirects, and asset URLs. Impact can look like SSRF—password reset theft, outbound calls to a poisoned host—without a raw socket sink.
Call it SSRF-adjacent if you want. Triage cares about the user-visible outcome.
Which header actually wins?
Apps behind CDNs and reverse proxies learn their public hostname from a mess of signals: Host, X-Forwarded-Host, Forwarded, X-Host, X-Original-URL, and framework-specific helpers. Your job is to find which one the app trusts when it builds absolute links.
Methodology that works for me:
- Trigger a flow that emits an absolute URL—password reset, invite email, OAuth redirect construction, sitemap, or canonical link.
- Send the request with a canary host in each forwarding header, one at a time.
- Read the email, redirect
Location, or HTMLhreffor your canary. - Repeat with duplicates:
X-Forwarded-Host: evil.example, legitimate.exampleand the reverse order.
If the reset link becomes https://canary.example/reset?token=..., you have a high-signal finding. Catch the token on a server you own. Do not phish real users.
Cache and middleware make it worse
When a CDN caches a response that embeds the poisoned host—Open Graph tags, script origins, password-reset pages served over GET—the bug becomes multi-victim. Check cache headers. A single poisoned fill plus a second client hit is stronger than one weird email.
Also watch internal routing. Some gateways route based on X-Forwarded-Host to tenant backends. Pointing it at localhost or an internal hostname can produce true SSRF-like fetches or admin vhosts. That is a different severity conversation; prove it with a safe internal canary path such as a unique static file on a host you are allowed to touch.
Absolute URL helpers in Rails, Django, Express, and Spring each have their own "trusted proxy" story. Misconfigured proxy counts mean a client-supplied X-Forwarded-Host is treated as authoritative. Dump the generated URL in a low-risk template first—password reset is the loudest demo, not always the first probe.
Do not overclaim
A reflected host in an error page is often informational. A host that lands in a signed email link, an OAuth redirect_uri validation bypass, or a cached script URL is the real report. Explain the trust boundary: proxy should set forwarding headers; application should ignore client-supplied copies and use a configured public base URL.
I've lost hours to apps that accept X-Forwarded-Host only on HTTP and strip it on HTTPS. Test both. Edge rules differ.
One more practical check: combine the poisoned host with password-reset rate limits and email-enumeration defenses. If the program already throttles resets, use an account you own and a single clean capture. Spamming the mailer to "strengthen evidence" is how good Host-header bugs get downgraded to noise.
When absolute links appear only in asynchronous jobs—invoice PDFs, digests, webhook retries—replay the triggering action after setting the header on the originating request. Some workers re-resolve the public host from config; others copy whatever the web tier stored. That split decides whether your canary ever leaves the browser-facing HTML.