SecureBlockLog inStart a pentest
Vulnerability Repository
HighAuthentication

Insecure Password Reset

Flawed password reset flows using predictable tokens, weak security questions, or missing expiry allow attackers to take over any account without knowing the current password.

CVSS 8.8CWE CWE-640OWASP A07:2021 — Identification and Authentication Failures

Description

Insecure password reset (CWE-640) encompasses a range of implementation flaws in the password recovery flow that allow an attacker to reset and take control of a victim's account without knowing their current password. Password reset is inherently high-risk because it is an out-of-band authentication path — it must securely establish identity using secondary factors (email possession, phone possession, security questions) that are often weaker than the primary authentication mechanism.

Common flaws include: predictable or enumerable reset tokens generated with weak pseudo-random number generators; tokens with no expiry or excessively long expiry windows; tokens that are not invalidated after a single use; reset flows that leak the reset link in HTTP Referer headers; security question implementations with guessable answers; host header injection in reset emails that redirects the reset link to an attacker-controlled domain; and race conditions where the token can be used simultaneously by attacker and victim.

This vulnerability class falls under OWASP A07:2021 (Identification and Authentication Failures) and is one of the highest-value targets in application penetration testing because it provides a direct path to account takeover for any user on the platform, including administrators.

How It Works

Weak token generation — an application generates reset tokens using a timestamp or predictable sequence:

# Vulnerable: token based on timestamp
import time
token = str(int(time.time()))  # 1723200000 — easily guessable

An attacker requests a reset for their own account, observes the token structure, then requests a reset for the target account and brute-forces the token within the valid time window.

Host header injection — the application builds the reset URL by trusting the Host header:

reset_url = f"https://{request.headers['Host']}/reset?token={token}"
send_email(user.email, reset_url)

The attacker sends a reset request with a spoofed Host header:

POST /forgot-password HTTP/1.1
Host: attacker.com

email=victim@example.com

The reset email is sent to the victim but contains https://attacker.com/reset?token=.... When the victim clicks the link, the token is sent to the attacker's server.

Token in Referer header — if the reset page loads third-party resources (analytics, CDN scripts) while the token is still in the URL:

GET /reset-password?token=abc123 HTTP/1.1
Referer: https://analytics.thirdparty.com

The token leaks in the Referer header to the third-party service.

No token invalidation — testing whether a token remains valid after use:

# Use token once
POST /reset-password token=abc123 new_password=attacker_password
# Use the same token again
POST /reset-password token=abc123 new_password=second_attacker_password → should fail but might not

Impact

  • Account Takeover — Taking over any user account, including administrators, by exploiting the reset flow
  • Privilege Escalation — Resetting an admin account password to gain administrative access to the application
  • Mass Account Compromise — Predictable or brute-forceable tokens enable automated takeover of many accounts simultaneously
  • Persistent Access — Changing the password and email of the compromised account to lock out the legitimate owner

Detection

  1. Test token predictability — request multiple reset tokens in quick succession for controlled test accounts. Analyze the tokens for patterns: sequential numbers, timestamps, base64-encoded values, short UUIDs, or other predictable structures.
  2. Check token expiry — request a reset token, wait 24 hours, and attempt to use it. Tokens should expire within 1 hour. Also test whether tokens expire after use.
  3. Test host header injection — send the forgot-password request with a modified Host header (X-Forwarded-Host, X-Host, X-Forwarded-Server) pointing to a Burp Collaborator domain. Monitor for HTTP callbacks containing the token.
  4. Test token reuse — use a reset token successfully, then immediately try to use the same token a second time. It should be invalidated after first use.
  5. Test for token in URL — if the reset token appears as a URL query parameter, check whether it leaks in server access logs, browser history, and Referer headers to third-party resources on the reset page.
  6. Test rate limiting — submit hundreds of guesses for a short token (/reset?token=000000 through 999999) to verify whether rate limiting or lockout prevents brute force.

Remediation

Generate cryptographically secure tokens. Use a CSPRNG to generate at least 128 bits of entropy, represented as a hex or base64 string:

import secrets
token = secrets.token_urlsafe(32)  # 256 bits of entropy

Set a short token expiry. Reset tokens should expire after 60 minutes and be invalidated immediately upon use. Store the expiry timestamp alongside the token hash in the database.

Invalidate tokens on use and on new request. Using a token must invalidate it. Requesting a new reset token must invalidate all previous tokens for that account.

Build reset URLs from configuration, not headers. Never use request.host or user-supplied headers to construct reset URLs:

# Safe
base_url = settings.BASE_URL  # e.g., "https://app.example.com"
reset_url = f"{base_url}/reset-password?token={token}"

Avoid tokens in URLs when possible. Deliver tokens via POST body submitted through a form, or use a short-lived one-time link where the token is in the path but immediately invalidated. Add Referrer-Policy: no-referrer to the reset page.

Rate-limit reset requests. Apply per-IP and per-account rate limiting on both the token request endpoint and the token submission endpoint.

Ready when you are
Scope a pentest in the next two minutes.
Start scoping