October 26, 2026
Predicting the Unpredictable: How a Weak Random Number Generator Let Me Forge Password Reset Tokens
Every password reset flow makes the same implicit promise: the link in your inbox is the only way in, because the token inside it could notโฆ

By T4nv1
4 min read
Every password reset flow makes the same implicit promise: the link in your inbox is the only way in, because the token inside it could not plausibly be guessed by anyone else. That promise rests entirely on one unglamorous piece of infrastructure โ whatever function actually generates the token โ and almost nobody questions it, because modern languages ship a correct, cryptographically secure random number generator by default. This bug existed because somewhere along the way, a developer reached for the wrong one.
The target, which I'll call Thistlewood here per their program's disclosure terms, is a mid-size e-learning platform with an entirely ordinary password-reset flow: submit an email, receive a link containing a token, click it, set a new password.
Why "Random" Isn't One Single Property
Most programming languages expose two meaningfully different categories of random number generation, and the distinction between them rarely matters until it suddenly matters enormously. General-purpose random functions โ the kind used for shuffling a list, picking a random quiz question, or generating sample data โ are built for speed and statistical distribution, not unpredictability against someone actively trying to guess the output. Cryptographically secure random generators are slower, deliberately, because they're built on entropy sources specifically designed to resist exactly that kind of prediction. Using the first category anywhere a genuine secret needs to be generated โ a password reset token, a session identifier, an API key โ is a well-documented, recurring mistake, precisely because the two functions often have nearly identical names and interfaces, differing only in which one a tired developer happened to autocomplete.
The First Thing That Looked Off
I requested several password resets in quick succession against test accounts I controlled, and logged each resulting token exactly as it appeared in the reset link:
a3f29c118d
a3f29c119e
a3f29c11b2
a3f29c11c7a3f29c118d
a3f29c119e
a3f29c11b2
a3f29c11c7Real cryptographic tokens, even when base-encoded into something that looks superficially similar, don't share long common prefixes like this across independent generations โ any shared structure at the start of several supposedly random values is an immediate, strong signal that whatever's generating them isn't drawing from a properly unpredictable source at all. The shared a3f29c11 prefix across four consecutive tokens, generated only seconds apart, pointed toward a generator seeded by something time-based and narrow, like the current timestamp in milliseconds, rather than genuine entropy.
Reconstructing the Generator's Behavior
Confirming this meant moving past "this looks suspicious" into actually reconstructing how the value was derived. I requested a larger batch of tokens, recording the precise server response timestamp alongside each one, and looked for a direct mathematical relationship between the two. After some experimentation, the pattern became clear: the token was a hexadecimal encoding of a value derived from the server's Unix timestamp in milliseconds at the moment of generation, run through a non-cryptographic pseudorandom function commonly used for exactly the wrong kind of purpose โ fast, deterministic, and fully reproducible if you know the seed.
With the timestamp as the seed and the same non-cryptographic algorithm reproduced locally, I could generate a candidate token for any given moment in time and compare it directly against a token I'd actually requested at that same moment. They matched, consistently, across repeated tests โ confirming the token wasn't drawing from any meaningful entropy pool at all. It was fully, deterministically derived from a value an attacker could observe or closely estimate simply by noting when the reset was requested.
From Prediction to Forgery
A deterministic generator seeded by timestamp means an attacker doesn't need to intercept a victim's email at all. If I know, or can closely estimate, the moment a victim's password reset was requested โ which an attacker can often trigger themselves, simply by submitting the reset request on the victim's behalf and noting the exact time โ I can compute the small range of candidate tokens the generator could plausibly have produced across the few hundred milliseconds of realistic timing uncertainty, and try each one directly against the reset-completion endpoint.
I tested this end-to-end using two of my own accounts: triggering a reset for the second account, noting the precise time of the request, computing the candidate token range locally, and submitting each candidate against the reset-completion endpoint for that account. Within a handful of attempts, one candidate succeeded, completing a password reset for an account without ever having received, intercepted, or had any access to the actual email containing the real link.
Keeping the Demonstration Contained
Both accounts involved throughout this test were ones I created and controlled. I did not attempt this technique against any other account, and I didn't explore how far the timing window could realistically be widened against a victim whose request time I could only estimate rather than observe directly โ confirming the core predictability was sufficient to make the case completely.
Writing the Report Clearly
Weak-randomness findings need to walk through the mathematical reconstruction explicitly, because "the tokens look similar" on its own doesn't convey severity the way actually demonstrating successful forgery does.
Title: Password reset tokens generated using a non-cryptographic, timestamp-seeded pseudorandom function allow token prediction and account takeover
Root cause: The password reset token generator derives its output from the server's millisecond-precision timestamp at generation time, processed through a fast, non-cryptographic pseudorandom algorithm rather than a cryptographically secure random source. An attacker who can observe or closely estimate the time a reset was requested can reconstruct the small set of candidate tokens the generator could have produced and test each directly.
Reproduction: The shared-prefix observation across sequential tokens, the timestamp-correlation analysis establishing the generator's deterministic behavior, and a full end-to-end forgery demonstrated entirely against two self-controlled test accounts.
Impact: Any attacker able to trigger or observe the timing of a victim's password reset request can forge a valid reset token without ever accessing the victim's email, achieving full account takeover.
Fix recommendations:
- Replace the token generator with a cryptographically secure random source, drawing sufficient entropy to make brute-force or prediction infeasible regardless of timing knowledge
- Invalidate any previously issued tokens generated under the vulnerable scheme, since their predictability doesn't improve simply because the generator has since been fixed
- Add rate limiting and anomaly detection around repeated reset-completion attempts, as defense in depth against any future weakness in token generation
What Happened After
The platform replaced the token generator with a properly cryptographically secure implementation and rotated all outstanding tokens as an immediate precaution, treating the fix as urgent given the complete authentication bypass it represented.
Why Weak Randomness Deserves a Dedicated Look on Every Target
Any token meant to function as a secret โ password resets, email verification, session identifiers, API keys โ is worth testing for exactly this pattern: generate several in quick succession, compare them for shared structure, and look for any correlation with observable values like server time. It's a cheap test, and when it succeeds, the resulting finding is almost always severe, because the entire security model of the feature rests on an assumption that turns out to be false.