CWICP study materials
CWICP study resources

Same-Second Reset Tokens and the Limits of Proof

CWICP · Put your knowledge to work in context.

All case studies

Same-Second Reset Tokens and the Limits of Proof

A fintech customer portal serves 42,000 users. Login is email plus password with optional TOTP, and the session cookie is a signed JWT. You have two authorised test accounts, alice.test@ and bob.test@. The password-reset flow emails a link containing ?t=<32 hex characters>. The token is valid for 24 hours, is not invalidated after a successful reset, and a successful confirm response sets a new session cookie for that account. Over 40 requests alternating between your two accounts, tokens requested within the same wall-clock second share their first 8 hex characters, and the two tokens differ only in their final 12 characters. The reset request endpoint throttles to 5 requests per minute per source IP and imposes no per-account limit. The rules of engagement forbid attacking real customer accounts, and the client wants a severity rating plus remediation guidance within 48 hours.

1. Can you determine whether the tokens are predictable without touching a customer account? Lay out the test and the decision points.

Read the worked answer

Predictability is a property of the generator, and the generator can be studied entirely with tokens issued to accounts you own.

Collection. Request 25-30 tokens for alice.test across several minutes, and the same for bob.test. For each, log the UTC second of the request (your clock, cross-checked against the server's Date header), the account, the token, and the response headers. Collection is free and touches nobody else.

Offline derivation. For each token, brute-force a ±120 second window (481 candidates) against a small set of constructions: MD5(email + epoch), MD5(epoch + email), truncated SHA-256, base64 of a timestamp and counter, java.util.Random seeded with System.currentTimeMillis() taking the next 8 bytes, and a UUIDv4 built from a broken generator. That is 481 x 30 x a handful of constructions - seconds of CPU, and it never hits /reset/confirm.

The decision point that matters. The shared 8-hex prefix only proves that a low-entropy component is shared: 8 hex characters is 32 bits, and if the remaining 24 characters are CSPRNG output, the token is fine. The question is whether the final 12 characters are derivable - a counter, a PRNG stream, or a second timestamp. Reproducing a whole token proves predictability; reproducing only a prefix is a reduced-entropy finding with a number attached, not a break.

Endpoint behaviour, recorded without abuse. Does confirm distinguish expired from invalid? Does a wrong token lock the account or reveal timing? Does a token issued for alice work when submitted with bob's address? Those answers decide whether guessing is reachable at all, and they are all obtainable with your own two tokens.

2. Assume the derivation is confirmed for your own accounts. What is the severity, and what will you refuse to do?

Read the worked answer

Severity is a function of the residual search space, not of the fact that the tokens look similar.

  • If the derivable part is 32 bits and the remainder is random: about 4.3 billion candidates. At 5 requests per minute per IP with no per-account lockout, one IP gets 7,200 attempts a day; a modest 100-IP set gets 720,000 a day, so roughly six days for a 50% chance against one specific account - and only inside a 24-hour validity window. That is Low-to-Medium, reported as a design weakness.
  • If a full token can be computed from email plus second: the account is takeable in one request, and the rating is Critical.
  • Independently of entropy, 24-hour validity with no invalidation after use means a token that leaks through a mail log, a shared inbox or a Referer header stays live for a day and is replayable. That is a genuine Medium or High finding on its own, and it is the one most likely to be missed.

What I will refuse to do. Once the generator is understood, the obvious next step is to request resets for real customers and submit guesses. I will not, not even to demonstrate impact. That is unauthorised access to 42,000 real accounts, it breaches the RoE, and it is unnecessary: a token reproduced for alice.test already proves the generator is deterministic. I also will not enumerate account existence through the reset endpoint beyond the two addresses I was given.

What I do instead. Report the severity with the arithmetic shown, offer a controlled demonstration against a synthetic account the client creates, and state explicitly which exploitation step I did not take and why - so the reader understands the rating is derived, not asserted.

3. Give the remediation and describe how you would retest it.

Read the worked answer

Fix. Replace the derivation with 128 bits from a CSPRNG (SecureRandom, secrets.token_urlsafe(32)), store only a SHA-256 hash of the token with the user id and expiry, mark it used atomically (UPDATE ... WHERE used_at IS NULL and check the row count), expire it after 15 minutes, and invalidate every outstanding token for the user on any successful reset and on password change. Bind the token to the account it was issued for so it cannot be submitted alongside a different email address. Add a per-account counter with exponential backoff - per-IP throttling does not survive a botnet. Do not issue a session cookie from the confirm response until the new password has been accepted.

Retest, in two parts.

Generator. Request 20 tokens across both test accounts over ten minutes and re-run the offline derivation harness against the new values. Assert that no candidate reproduces any token, and that two tokens requested in the same second share no prefix. Include the harness output in the retest annex so the client can see the search space was actually covered, not merely sampled.

Behaviour. Assert four properties: reuse of a consumed token returns a generic failure; a token issued for alice is rejected when submitted with bob's address; a token older than 15 minutes is rejected; and a password change invalidates all previously issued tokens and existing sessions. Each assertion maps to one line of the remediation, so a regression in any single control is visible in CI rather than only in the next penetration test.