Choose your stack
Fix insecure randomness for reset tokens in Node.js and Express
Generate the token with a CSPRNG: crypto.randomBytes(32).toString('base64url') is 256 bits (prefer it over crypto.randomUUID, which carries only 122).
Open the fix card Python · FlaskFix insecure randomness for reset tokens in Python and Flask
Generate the token with the secrets module: secrets.token_urlsafe(32) is 256 bits from the OS CSPRNG.
Open the fix card Java · Spring BootFix insecure randomness for reset tokens in Java and Spring Boot
Generate the token with java.security.SecureRandom: 32 bytes (256 bits) Base64url-encoded.
Open the fix card C# · ASP.NET CoreFix insecure randomness for reset tokens in C# and ASP.NET Core
Generate the token with RandomNumberGenerator.GetBytes(32) (256 bits), Base64Url-encoded.
Open the fix card Go · net/httpFix insecure randomness for reset tokens in Go and net/http
Generate the token with crypto/rand: rand.Read into 32 bytes, encoded with base64.RawURLEncoding, is 256 bits from the OS CSPRNG (rand.Text() gives at least 128).
Open the fix cardStacks without a card
- TypeScript and Next.js: no card yet. The nearest is Fix insecure randomness for reset tokens in Node.js and Express, in the same language.
- Python and FastAPI: no card yet. The nearest is Fix insecure randomness for reset tokens in Python and Flask, in the same language.
- Python and Django: no card yet. The nearest is Fix insecure randomness for reset tokens in Python and Flask, in the same language.
- Ruby on Rails: no card yet. The nearest is Fix insecure randomness for reset tokens in Node.js and Express.
- PHP and Laravel: no card yet. The nearest is Fix insecure randomness for reset tokens in Node.js and Express.
How it happens
- The code builds a token from a general-purpose PRNG. These generators are built for speed and reproducibility, not secrecy: java.util.Random and System.Random are linear congruential or subtractive generators whose entire internal state is recoverable from a handful of outputs, and V8's Math.random and Python's random.random (a Mersenne Twister) are likewise reconstructable. Once the state is known, every past and future token is known.
- A seed is chosen that the attacker can guess. new Random(System.currentTimeMillis()), random.seed(int(time.time())), new Random(DateTime.Now.Millisecond) or a copied mulberry32 seeded with Date.now() all tie the token to the clock. The attacker, who often triggered the reset themselves, knows that time to within a few milliseconds and brute-forces the small remaining space.
- The token is predicted from its neighbours. Because a PRNG is deterministic, an attacker who can mint tokens for an account they control (a sign-up, their own reset) reads off consecutive outputs, recovers the generator state, and predicts the very next token, which is the victim's.
- Someone 'adds security' by hashing the weak value: SHA-256(timestamp) or SHA-256(prng()). The output looks like 256 random bits, but a hash is deterministic and adds no entropy, so the search space is still just the timestamp or the PRNG seed. The hash only hides the weakness from a glance, not from a brute force.
- The token is then compared with == or String.equals, which returns as soon as two characters differ. Against an attacker who can measure response time this leaks the token one position at a time (CWE-208); a reset or session token must be checked in constant time.
Fix principles
- Generate tokens from a cryptographically secure RNG, never a general-purpose one: crypto.randomBytes or crypto.randomUUID in Node, secrets in Python, java.security.SecureRandom in Java, System.Security.Cryptography.RandomNumberGenerator in .NET. ASVS 5.0 (11.5.1) asks for at least 128 bits of entropy.
- Take at least 128 bits, and prefer more: these cards use 32 bytes (256 bits). A v4 UUID carries only 122 bits once its version and variant bits are fixed, which ASVS 11.5.1 explicitly calls out as short of the bar, so prefer 32 random bytes over randomUUID for a token.
- Never seed a generator with the clock, a counter, a process ID or any value an attacker can observe or guess, and never 'strengthen' a weak value by hashing it: SHA-256 of a timestamp has exactly as much entropy as the timestamp.
- Compare the presented token with the stored one in constant time (crypto.timingSafeEqual, hmac.compare_digest, MessageDigest.isEqual, CryptographicOperations.FixedTimeEquals); hashing both sides to a fixed length first avoids leaking the length. Store only a hash of the token, give it a short expiry, and make it single use.
- Make the RNG robust under load (ASVS 11.5.2): the OS CSPRNG these APIs wrap does not deplete or block in the way folklore suggests, so there is no reason to fall back to a weak generator under heavy demand.
How the cards prove it
- Run each card's tests in three modes: the fixed implementation must pass, and the vulnerable and the plausible-but-wrong implementations must fail. run-all.mjs does this for every card.
- The exploit tests act as an attacker who can trigger the victim's reset. They reproduce the token two ways -- replaying or recovering the PRNG (java.util.Random state recovery in Java, a seed or small-seed-space replay elsewhere) and brute-forcing SHA-256 of the issue-time window -- and then try the forged token against /reset/confirm. Against the fixed code every attempt fails; against the vulnerable code the PRNG attack wins, and against the wrong code the hashed-timestamp attack wins. Node and Python also spy on the comparison to confirm it is constant time.
- Scanners catch only part of this. Bandit's B311 flags Python's random module and .NET's CA5394 flags System.Random, but Semgrep's Java weak-random rule only matches a method called directly on new Random(...) and misses a stored instance, its default pack found nothing on a hand-rolled PRNG seeded with Date.now(), and CodeQL's js/insecure-randomness and java/insecure-randomness need the taint to start at a source they model. Each card's scannerRules records exactly what ran and what it found; the regression tests are the real control.
References
- OWASP Top 10:2025 A04 Cryptographic Failures
- CWE-330: Use of Insufficiently Random Values
- CWE-338: Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG)
- CAPEC-59: Session Credential Falsification through Prediction
- CAPEC-112: Brute Force
- OWASP ASVS 5.0.0, V11.5 Random Values (11.5.1: CSPRNG and at least 128 bits of entropy; UUIDs do not qualify)
- OWASP Cheat Sheet: Secure Random Number Generation (Cryptographic Storage / Session Management)
- OWASP WSTG: Testing for Weak Session Token Generation / Weak PRNG
- NIST SP 800-90A Rev. 1: Recommendation for Random Number Generation Using Deterministic Random Bit Generators
Attack patterns:
Practice in the browser
- Insecure randomness lab: Generate unguessable API keys · Practitioner, JavaScript