1

Detection

Predictable Reset Tokens

πŸ”Ž

Detection

A password-reset token needs to be cryptographically unguessable, because possession of the token effectively grants temporary authority to reset the account's password.

A vulnerable implementation may derive the token from predictable information:

// Weak β€” derived from predictable values
$token = md5($user->email . time());
// Correct β€” independently generated cryptographic randomness 
$token = bin2hex(random_bytes(32));

The problem with the first implementation isn't simply that it uses MD5. The fundamental problem is that the token is derived from values an attacker may know or be able to predict.

If an attacker knows the victim's email address and can narrow down when the reset token was generated, they may be able to reproduce candidate tokens without ever intercepting the original reset message.

The same problem can occur with other predictable inputs:

username + timestamp
email + timestamp
user ID + timestamp
incrementing database ID
static secret + user-controlled value

Hashing these values does not make them unpredictable. A cryptographic hash is deterministic: if the attacker can reconstruct the same input, they can calculate the same output.

A secure implementation instead generates the token independently using a cryptographically secure random number generator:

$token = bin2hex(random_bytes(32));

The server should then associate the token with the appropriate account and enforce a limited expiration period and single-use behavior.


The distinction is:
Hashing predictable data β†’ predictable output
Cryptographically random generation β†’ unpredictable token

2

Exploitation

Token Leakage

πŸ”¬

Exploitation

A properly generated reset token can still be compromised if the application accidentally exposes it through surrounding infrastructure. Unpredictability prevents guessing; it does not prevent disclosure.

A common example is placing the token directly in a URL:

/reset-password?token=8f3a...

URLs can be recorded in server access logs, browser history, proxy logs, analytics systems, and other infrastructure that processes requests.

The token can also potentially leak through the Referer header. If the reset page loads a third-party resource β€” such as an analytics script, advertising resource, or externally hosted asset β€” the browser may disclose information about the originating URL to that third party depending on the configured referrer policy.

Another surprisingly common failure is exposing the token directly through an API response:

{
    "email": "user@example.com",
    "reset_token": "8f3a..."
}

This may be introduced during development or debugging and accidentally remain enabled in production. Anyone who can access that API response effectively obtains the credential needed to reset the account.

The broader problem is that reset tokens often travel through many more systems than developers initially consider:

Token generated β†’ Email / reset URL β†’ Browser β†’ Application β†’ Logs / analytics / proxies / third parties

Every unnecessary place where the token appears creates another opportunity for disclosure.

Treat a reset token like a password: don't put it in logs, unnecessary responses, third-party requests, or other places where it can be copied or retained.

3

Exploitation

Token Reuse

πŸ”¬

Exploitation

A password-reset token should be permanently invalidated immediately after it is successfully used. Checking only whether the token is still within its expiration window is not sufficient.

A vulnerable implementation might effectively do this:

if (token_is_valid_and_not_expired) {
    allow_password_reset();
}

If the server does not separately record that the token has already been consumed, the same token may remain usable until its normal expiration time.

For example, an attacker might recover an old reset token from a compromised email account or a shared computer's browser history. If the legitimate user has already used that token to regain access, the attacker may still be able to submit the same token again while it remains valid.

A secure flow instead treats the reset token as a single-use credential:

Token issued β†’ Token verified β†’ Password reset succeeds β†’ Token permanently invalidated β†’ Same token rejected

The important distinction is:

Expiration: β€œIs this token still within its allowed lifetime?”

Consumption: β€œHas this token already been successfully used?”

Both checks are necessary.

4

Vulnerability

Reset Token Expiration

⚠️

Vulnerability

A password-reset token should have a limited validity window so that a leaked or intercepted token becomes useless relatively quickly.

A common implementation uses a lifetime measured in minutes β€” often somewhere around 15 to 60 minutes, depending on the application's risk profile and user experience requirements. The exact value matters less than the underlying principle: the token should remain valid only for as long as reasonably necessary to complete the reset.

A vulnerable implementation might allow a token to remain valid for days or weeks, or worse, fail to enforce expiration altogether:

Token issued
    ↓
Day 1  β†’ still valid
Day 7  β†’ still valid
Day 30 β†’ still valid

A shorter validity period limits the damage window:

Token issued β†’ Short validity window β†’ Token expires β†’ Leaked token becomes useless

Expiration should be enforced server-side using a trustworthy timestamp rather than relying on anything supplied by the client. The expiration check should also be combined with the other reset-token properties covered in this lesson:

  • cryptographically unpredictable generation

  • single-use enforcement

  • secure handling and storage

  • appropriate invalidation after successful use

5

Vulnerability

Host/Header Manipulation

⚠️

Vulnerability

Also known as password reset poisoning, this vulnerability occurs when an application constructs a password-reset URL from an attacker-influenced HTTP Host header instead of using a fixed, trusted application domain.

For example:

// Vulnerable β€” trusts an attacker-influenced Host header
$resetUrl = "https://" . $_SERVER['HTTP_HOST'] .
            "/reset-password?token=" . $token;
// Correct β€” uses a server-configured trusted domain 
$resetUrl = config('app.url') . 
                    "/reset-password?token=" . $token;

The important point is that the reset token itself may be perfectly secure. It can be cryptographically random, short-lived, and single-use. The vulnerability is that the application places that valid credential into a URL whose destination is controlled by the attacker.

Conceptually:

Attacker-controlled Host
          ↓
Application generates legitimate reset token
          ↓
Reset email contains attacker-controlled domain
          ↓
Victim or automated system requests the link
          ↓
Token may be disclosed to attacker
          ↓
Attacker submits token to the real application
          ↓
Password reset

For example, if the application accepts an attacker-supplied host such as:

Host: attacker.example

it might generate:

https://attacker.example/reset-password?token=VALID_TOKEN

The attacker does not need to predict or forge the token. They only need the application to place the legitimate token into a URL pointing somewhere they control.

The secure design is to never derive security-sensitive URLs from untrusted request headers. The reset domain should come from trusted server-side configuration, and the deployment should also validate or constrain accepted hostnames at the appropriate infrastructure layer.

6

Exploitation

Account Takeover Through Reset Flows

πŸ”¬

Exploitation

The real-world danger of password-reset vulnerabilities is often not any single weakness, but how multiple weaknesses can be chained into a complete account takeover.

Consider a realistic attack path:

Username enumeration
        ↓
Confirm target account exists
        ↓
Trigger password reset
        ↓
Host-header poisoning
        ↓
Valid reset token sent to attacker-controlled destination
        ↓
Token captured
        ↓
Weak expiration / token reuse
        ↓
Token remains usable
        ↓
Attacker resets the victim's password
        ↓
Account takeover

Notice what happened: the attacker never needed to discover the victim's original password. The password-reset mechanism itself became an alternative authentication path.

Each weakness contributes something different:

  • Username enumeration identifies a valid target.

  • Host-header poisoning exposes the legitimate reset credential to the attacker.

  • Long-lived tokens increase the window in which the stolen credential can be used.

  • Token reuse allows the credential to remain useful even after the legitimate user has already completed a reset.

A reset flow should therefore maintain these properties throughout its lifecycle:

Unpredictable
     ↓
Securely delivered
     ↓
Short-lived
     ↓
Single-use
     ↓
Properly invalidated
     ↓
Password reset completed
7

Summary

Key Takeaways

Summary

Password reset exists specifically to let someone regain access without their original credential β€” which is exactly why every control around it must be treated as security-critical rather than as an afterthought.

Token randomness, strict single-use enforcement, short expiration, and constructing reset links from a trusted server-side domain all protect the same underlying security property: only the legitimate account owner should be able to exercise the reset capability.

The normal password may be protected by strong hashing, MFA, rate limiting, and other controls, but none of those matter if an attacker can simply bypass them through a poorly secured recovery flow.