Reflecting an injected Host header is the first step - confirming that value reaches a password-reset link or a redirect target is the step that decides severity.
Applications lean on the Host header more than their authors remember. Password-reset links, email confirmation paths and self-referencing redirects are often built from the request host rather than a configured base URL. Reflect an injected host and you have reachability; show that the injected value reaches one of those sinks and you have decided the severity. The gap between the two is the entire finding.
A framework that writes https://{request.host}/reset?token=... is trusting a client-supplied value to build a link it will email to a real user. Change the host and you change where that link points - and where the token travels when the user clicks.
The evidence bar is a benign, nonce-bearing hostname - never a real attack domain, never a payload that executes - checked against each sink in sequence. Seeing the nonce echoed in the body is step one. Seeing it become the origin of a reset link or a Location header is the step that raises the finding.
# request a reset with an injected, benign nonce host POST /account/reset HTTP/1.1 Host: ap-probe-62615533.example X-Forwarded-Host: ap-probe-62615533.example email body / captured link: https://ap-probe-62615533.example/reset?token=... # the nonce host reached the link builder == confirmed, not just reflected
Whether X-Forwarded-Host is honoured by default varies by framework, and a CDN in front adds another rewrite. A probe validated against one stack quietly fails against another, so the engine tests the behaviour rather than assuming it.
| Framework | X-Forwarded-Host by default | Common pitfall |
|---|---|---|
| Rails | Honoured unless host authorization is set | Missing config.hosts allowlist |
| Spring | Respected when behind a forwarded-headers filter | ForwardedHeaderFilter trusting unvalidated proxies |
| Django | Gated by ALLOWED_HOSTS | Wildcard ALLOWED_HOSTS in non-debug deploys |
| Express | Reads req.headers.host directly | Trusting the header with no proxy validation |
A controlled host in a password-reset link is high - the token leaves with the user. A host that only lands in a redirect target is a medium open redirect. A host that is merely echoed in a page body, reaching no dangerous sink, is a note. We rate the sink we proved, never the echo we saw.
Build links from a configured base URL and validate the host against an allowlist. The header stops being an input the attacker can steer, and the reset link goes back to pointing where it should.