Insecure Randomness
detect, understand, remediate
Insecure randomness covers any security-sensitive value generated by a non-cryptographic random number generator. Session identifiers from Math.random, password reset tokens from java.util.Random, OAuth state from rand(), or CSRF tokens seeded from a process timestamp all share the same shape: the algorithm is deterministic, the seed is recoverable, and an attacker who watches a few outputs can predict the next one. The library call returns a random-looking number; the security guarantee never lands.
No credit card required. Free plan available forever.
What is insecure randomness?
Insecure randomness (CWE-330 Use of Insufficiently Random Values, with subclasses CWE-331 Insufficient Entropy and CWE-338 Use of Cryptographically Weak Pseudo-Random Number Generator) is the use of a non-cryptographic random number generator to produce a value that the application treats as unguessable. Session identifiers, password reset tokens, OAuth state parameters, CSRF tokens, password salts, encryption initialisation vectors, API keys, MFA backup codes, and one-time passwords all need to come from a source an attacker cannot reproduce. When the source is Math.random in JavaScript, java.util.Random in Java, rand() or mt_rand() in PHP, srand+rand in C, or System.Random in older .NET, the source is reproducible. The token looks random; it is not.
The classification sits inside OWASP A02:2021 Cryptographic Failures, next to but distinct from weak cryptography, which covers algorithm and mode choice, and TLS/SSL misconfiguration, which covers transport-layer choices. Insecure randomness is about the source of the bytes the application feeds into those algorithms. A correct AES-GCM call with a Math.random nonce is still broken; the cipher is sound, the nonce is not. A bcrypt password hash with a salt from java.util.Random is still broken; the hash function is sound, the salt is predictable across registrations and lets an attacker precompute rainbow tables for the entire user base.
On a real engagement the finding rarely ships alone. Predictable session ids hand an attacker the next valid session without credentials, which is broken authentication. Predictable password reset tokens hand the attacker an account, which is password reset territory at the protocol layer and account takeover at the business layer. Predictable JWT signing keys generated from a non-CSPRNG turn the entire token issuance into a forgery surface, which is JWT territory. The bug class sits upstream of all of these.
How insecure randomness fails in practice
Spot the call site
Source review, traffic analysis, or string-search across the codebase identifies the RNG: Math.random(), new java.util.Random(), rand(), srand(time(NULL)) plus rand(), mt_rand(), uniqid() in PHP, GUID-as-token shapes that come from the system tick counter, or a custom xorshift the team wrote in a utilities module.
Confirm the value is security-sensitive
A random colour for a UI animation is fine. A random session id, password reset token, OAuth state, CSRF token, MFA backup code, password salt, IV, nonce, or API key is not. The classification depends on what the value protects, not on the function name.
Recover the seed or the state
Mersenne Twister (the default in many languages) leaks its full 624 word state after 624 outputs. Linear congruential generators leak after 2 outputs. A timestamp seed (srand(time(NULL))) is recoverable to the second. Math.random in V8 is recoverable from a small handful of doubles. The exploit shape is brute-forcing or solving for the seed, then forwarding the generator to predict the next output.
Demonstrate impact
The proof is the next valid token, generated offline, that the server accepts. A working session, a hijacked password reset, a forged OAuth flow, or a salt collision that turns a hashed password database into a rainbow-table target. The CVSS vector follows the data the predicted token unlocks.
The shapes that show up on real engagements
Each row below maps a non-cryptographic API to the security-sensitive context where it commonly leaks into production. Encountering any of these on a pentest is enough to open a finding before the exploitation work begins.
| Language | Insecure call | Where it usually leaks | Correct replacement |
|---|---|---|---|
| JavaScript | Math.random() | Session ids, password reset tokens, CSRF tokens, share-link tokens, API keys generated client-side or in legacy Node code. | crypto.getRandomValues() (browser), crypto.randomBytes() (Node) |
| Java | new java.util.Random() | Session ids in older Servlet code, password reset tokens, salts, API tokens, OAuth state when developers reach for nextLong() instead of SecureRandom. | java.security.SecureRandom |
| PHP | rand(), mt_rand(), uniqid(), lcg_value() | Password reset tokens (the famous classic), CSRF tokens, file-upload paths, password salts in legacy code that pre-dates password_hash. | random_bytes(), random_int() |
| Python | random.random(), random.randint(), random.choice() | Session keys in older Flask and Django apps, MFA backup codes, password reset tokens, invitation links written before the secrets module landed. | secrets.token_bytes(), secrets.token_urlsafe(), secrets.choice() |
| .NET | new System.Random() | Session tokens, anti-forgery tokens in legacy Web Forms code, license keys, password reset codes, IVs. | RandomNumberGenerator.Create(), RandomNumberGenerator.GetBytes() |
| C / C++ | srand(time(NULL)); rand() | Auth tokens, IVs, key material in embedded contexts where developers default to the standard library RNG. | /dev/urandom, getrandom(), BCryptGenRandom on Windows |
| Go | math/rand | Tokens generated by code that imports the wrong package; the Go standard library separates math/rand from crypto/rand and the wrong import is a frequent bug. | crypto/rand |
| Ruby | rand, Random.rand, Kernel#rand | Session tokens in legacy Rails apps, password reset tokens before SecureRandom landed in the standard helpers. | SecureRandom.hex(), SecureRandom.urlsafe_base64() |
Why insecure randomness really fails
A pseudo-random number generator (PRNG) is a deterministic algorithm. Given the same seed, it produces the same sequence. The cryptographic question is not whether the output looks random to a casual observer, it is whether an attacker who can see a few outputs can recover enough internal state to predict the next ones. Non-cryptographic PRNGs are designed for speed, statistical uniformity, and simulation use cases. They are not designed to resist state recovery.
Mersenne Twister state recovery
MT19937 is the default in Python random, PHP mt_rand, Ruby rand, and many others. Its internal state is 624 32-bit words. After 624 consecutive outputs, the full state is recoverable in linear time, and the next output is deterministic. On a target that exposes a stream of MT outputs (any place the user can request multiple tokens, including pagination cursors and form ids), 624 outputs is a small ask.
Time-seeded generators are seconds-recoverable
srand(time(NULL)) seeds the C library RNG with the Unix timestamp. An attacker who knows the rough server time at token issuance enumerates the seed by trying every second in a window, regenerates the sequence, and matches against the issued token. The exploit fits in a short script and runs in seconds.
Math.random is V8 state recoverable
Modern V8 implementations use xorshift128+. The state is 128 bits, but a small handful of consecutive Math.random outputs (typically 5 or fewer doubles) is enough to recover the state via constraint solving (a published technique using Z3). Once the state is recovered, all past and future outputs in that V8 instance are predictable.
Linear congruential generators leak instantly
The classic rand() implementation on older platforms is a 31-bit or 32-bit LCG. Two consecutive outputs (or fewer with known constants) recover the seed. There is no work to do; the algorithm is literally invertible.
Reseeding with weak entropy does not help
A common mistake is to reseed a non-CSPRNG with bytes from time, process id, or a memory-address derivative. The reseed is reproducible by an attacker who knows the rough boot time and pid range. Reseeding a flawed generator with weak entropy is a flawed generator.
Hashing the output does not save it
sha256(rand()) is still vulnerable. Once the attacker recovers the RNG state, they replay the same hash. The hash is deterministic; the input is the predictable part. The fix is to fix the source, not to wrap it.
A worked example: predictable password reset tokens
A common shape on engagements is a password reset endpoint that generates a token, stores it against the user account, and emails a link with that token to the registered address. The reset handler accepts the token and serves the new-password form. The implementation, in legacy PHP, uses md5(uniqid()) to produce the token. Both the hash function and the entropy source are wrong, but the entropy source is the part that matters here.
// Vulnerable handler (PHP)
function generate_reset_token($user) {
$token = md5(uniqid('reset_', true));
save_token($user, $token);
mail($user->email, 'Reset your password', "https://app.example/reset?t=$token");
}
// uniqid() is documented as "based on the current time in microseconds".
// The format is a 14-character hex prefix derived directly from microtime().
// The "more entropy" flag adds a 10-char LCG suffix that does not save it.
// An attacker who can request a reset and observe the issuance time within
// a few seconds can enumerate every microtime() in that window, regenerate
// every uniqid(), hash each one, and arrive at the issued token offline.The pentester triggers a reset for the target account. They request a reset for their own attacker-controlled account at the same time, capture the token in their inbox, and learn the microtime at issuance to within a few microseconds. They enumerate every microtime in a one-second window around the target reset, generate every uniqid output, take md5 of each, and stop when they hit the token they captured. The same enumeration applied to the target account returns a token that the server accepts. The reset URL works on the first try. No credential phishing, no email server compromise, no session theft; the token itself was guessable.
The fix is structural: switch to random_bytes(32) (or its language equivalent), serialise as base64url or hex, store a hash of the token rather than the token itself, and bind the token to a single-use validity window. On a SecPortal engagement, the proof of impact is the captured reset email plus the offline-generated token plus the timestamped server log entry showing the reset accepted on the first attempt. CVSS 3.1 is calibrated to the highest-privilege account the predictable token reaches; for a self-service reset on a regular user this typically lands at AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N (around 8.1), and rises if the predictable flow targets administrative accounts. The finding stays attached to the engagement record with the token enumeration script, the request, the email, and the log line through retest.
How to detect insecure randomness
Automated detection
- SecPortal's code scanner runs Semgrep with rulesets that flag Math.random, java.util.Random, rand, mt_rand, uniqid, srand, lcg_value, System.Random, math/rand imports in Go, and Kernel#rand in Ruby across security-sensitive call sites. Token generation, password reset flows, salt creation, IV creation, and OAuth state generation are all in scope.
- The authenticated scanner samples password reset tokens and session identifiers from real flows, runs entropy analysis (Shannon entropy plus distribution checks), and flags tokens whose statistical profile is inconsistent with a CSPRNG output. Sequential ids, time-correlated ids, and ids whose first bytes change predictably across requests are surfaced as findings.
- SCA dependency scanning catches older versions of token-generation libraries where the default RNG was non-cryptographic and a later release switched to a CSPRNG. Examples include older versions of jsonwebtoken, certain Java session managers, and PHP frameworks that pre-date random_bytes.
- The same scanner pipeline flags suspicious co-occurrence patterns: an RNG call followed by a hash function (md5 or sha256) immediately fed into a token field, an RNG call seeded from time or process id, and any encoder pattern that wraps a non-CSPRNG output as a security token.
Manual testing
- Inventory every endpoint that issues a token: signup, login, password reset, email verification, magic link, OAuth state, CSRF token endpoint, file-share link, invitation, MFA backup code generation. Each one is a candidate for a non-cryptographic source.
- Sample a hundred tokens from each endpoint by triggering the flow programmatically. Run the bytes through dieharder, ent, or a NIST randomness suite. A high chi-square deviation, a low Shannon entropy, or visible structure (timestamp prefixes, sequential bytes, consistent low-byte patterns) is evidence of a non-cryptographic source.
- For Mersenne Twister suspects, request 624 outputs and use a published state-recovery script (untwister, mt19937-recover) to recover the internal state. If the recovery succeeds and the next predicted output matches a held-out sample, the finding is conclusive.
- For time-seeded suspects, request a token with a known-precision server timestamp visible in the response (Date header, response body timestamp, log artefact). Enumerate every seed value across a one-second window, generate the sequence, and match the issued token against the candidates.
- For Math.random suspects (typically client-side or older Node code), capture five consecutive doubles and feed them to a published V8 state-recovery technique. The recovery script returns the internal state; subsequent Math.random outputs are then deterministic from that point.
- If source is in scope, grep for the language-specific patterns in the table above. Each hit is a finding once the call site is mapped to a security-sensitive context. Some hits are intentional (a UI animation seed) and can be excluded with a comment; the rest are reportable as written.
How to fix insecure randomness
Switch to the platform CSPRNG for every security-sensitive value
crypto.getRandomValues in browsers, crypto.randomBytes in Node, java.security.SecureRandom in Java, secrets.token_bytes in Python, random_bytes in PHP 7+, RandomNumberGenerator in modern .NET, crypto/rand in Go, SecureRandom in Ruby. The replacement is a one-line change at most call sites and removes the entire bug class for that token.
Set a minimum token length tied to the threat model
128 bits of CSPRNG output is the standard floor for session and reset tokens. 256 bits for long-lived API keys and recovery codes. Encode as base64url for URL-safe transport. Anything shorter than 128 bits invites brute force; anything reused invites correlation.
Hash tokens at rest, not in the database table
Store sha256(token) rather than the token itself. Compromise of the database does not then yield working tokens. Verification compares the hash of the presented token against the stored hash, with constant-time comparison to avoid a separate timing oracle.
Bind tokens to context and to a short lifetime
A reset token binds to the user id, the issuance time, the intended action, and a single-use flag. A session token binds to the issuance ip subnet or the user agent class where the threat model justifies it. A short expiry plus single-use semantics removes the token-replay surface even if entropy is unexpectedly weak.
Audit every RNG call site, not just the obvious ones
A grep across the codebase for the patterns in the table above is usually enough to find the long tail. Every call site that produces a value the application later treats as unguessable is a candidate. Salts, IVs, nonces, password reset tokens, invitation tokens, magic-link tokens, OAuth state, CSRF tokens, anti-replay nonces, idempotency keys, and webhook signing secrets all matter.
Move RNG selection out of application code where possible
Modern frameworks (Spring Security, Django auth, Rails has_secure_token, ASP.NET Core anti-forgery) expose token primitives that are CSPRNG-backed by default. Using the framework primitive is one less place to introduce a regression and one less audit step at the next pentest.
Plan for entropy in constrained environments
Embedded targets, container start storms, and serverless cold starts can deplete entropy pools. Use getrandom or BCryptGenRandom (which block on insufficient entropy) rather than reading directly from /dev/random or /dev/urandom in environments that may have a thin entropy pool. The goal is a CSPRNG that gates on real entropy at boot.
Migrate existing tokens after the fix
Switching the RNG in code does not retroactively rotate the tokens already issued. Force a rotation of long-lived tokens, invalidate existing sessions where the threat model justifies a single sign-out event, and rerun the issuance audit on the next release. The fix is incomplete until legacy tokens have aged out.
Reporting an insecure-randomness finding
Insecure-randomness findings get disputed in two predictable ways. Engineering pushes back that the token is "already long enough to be unguessable", missing that length without entropy is decoration, or that the function name has random in it so the function must be random. The strongest reports name the exact call site (file, line, function), the RNG family in use, the operation it supports (session id, password reset, OAuth state, salt, IV, MFA backup code), and the realistic attack model that follows: state recovery for MT, time enumeration for srand, V8 solving for Math.random, or LCG inversion for legacy rand.
On a SecPortal engagement, the finding sits on the engagement record with the affected endpoint and call site, the captured token sample, the entropy analysis output, the proof-of-concept script that recovers the state and predicts the next token, the CVSS 3.1 vector calibrated to the data the predicted token unlocks, the CWE-330 mapping (and CWE-338 where the source is specifically a non-CSPRNG), and the remediation guidance from this page. Pentest engagement records keep the finding linked to the original commit and the retest verification, so the close-out conversation references the actual RNG migration rather than a vague "tokens are random now" ticket. The finding triage workflow covers how to separate scanner-derived flags (a Math.random import in source) from manually validated findings (a captured next-token prediction), so the report differentiates rule hits from confirmed exploits, and the pentest report writing guide covers how to phrase the business impact for a reader who needs to understand why a function called random is not random in the cryptographic sense.
Compliance impact
OWASP Top 10
A02 Cryptographic Failures
OWASP ASVS
V6 Stored Cryptography
PCI DSS
Req. 3 and 6 Strong Cryptography
ISO 27001
A.8.24 Use of Cryptography
SOC 2
CC6.1 Logical Access
NIST 800-53
SC-13 Cryptographic Protection
NIST SP 800-90A
Random Bit Generators
CREST Pentesting
Cryptography Test Methodology
A pentester checklist for insecure randomness
The list below is the minimum coverage a tester should walk before declaring a target's token surface acceptable. Each item maps to a specific finding shape, with a CVSS profile that reflects the data the predicted value unlocks.
- Inventory every endpoint that issues a token: login, signup, password reset, email verification, magic link, invitation, MFA backup code, OAuth state, CSRF token, file-share link, anti-replay nonce, webhook signing secret, idempotency key. Treat each one as a candidate non-CSPRNG sink until proven otherwise.
- Sample 100 tokens per endpoint and run them through dieharder, ent, or a NIST randomness suite. Note the Shannon entropy, the chi-square score, and any visible structure such as timestamp prefixes, sequential ranges, or consistent low-bit patterns.
- For Mersenne Twister suspects, capture 624 consecutive outputs and run a state-recovery tool such as untwister or mt19937-recover. Verify the prediction against a held-out sample. A successful prediction is a conclusive finding.
- For time-seeded suspects, enumerate seeds across a one-second window using the response Date header or any visible server timestamp, regenerate the sequence, and match the issued token. A correct match is a conclusive finding.
- For Math.random suspects in client-side or legacy Node code, capture a small set of consecutive doubles and run a V8 state-recovery solver. A correct prediction of the next double indicates an exploitable RNG even if the application then hashes the output.
- If source is in scope, grep for Math.random, java.util.Random, rand, mt_rand, uniqid, lcg_value, srand, System.Random, math/rand, Kernel#rand. Map every hit to the security context (token, salt, IV, nonce, key) and report each one as a finding unless the call site is demonstrably non-security-sensitive.
- Verify password storage uses a CSPRNG salt. A salt that comes from a non-CSPRNG turns the password database into a precomputable target even if the hash function (bcrypt, argon2) is correct.
- Verify password reset and MFA backup code flows specifically. These are the two highest-impact contexts for predictable tokens because each one is a direct path to account takeover without any credential phishing.
- Record the CVSS vector calibrated to the data the predicted token unlocks (Confidentiality:High and Integrity:High when the token gates account access; Confidentiality:High alone when it gates a read-only resource), the CWE mapping (CWE-330 for insufficient randomness, CWE-338 for use of a non-CSPRNG, CWE-331 for insufficient entropy), the affected endpoint and call site, the entropy analysis or state-recovery proof, and the remediation plan covering the CSPRNG migration, the token length floor, and the at-rest hashing convention.
Find predictable tokens before the next session is forged
SecPortal SAST flags Math.random, java.util.Random, mt_rand, and rand() in security-sensitive call sites, the authenticated scanner samples password reset and session tokens for entropy, and findings keep the seed analysis attached through retest. Start free.
No credit card required. Free plan available forever.