CWE-338: Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG)
Overview
Weak PRNG occurs when applications use cryptographically insecure random number generators for security-sensitive operations, making it possible for attackers to predict or reproduce random values.
Relationship to Other CWEs
- CWE-338 (this page) - a cryptographically insecure generator used for a security-sensitive value, so an attacker can predict or reproduce what it produced.
- CWE-330 (Use of Insufficiently Random Values) - the parent of this CWE, and the page to start from when it is not yet settled which randomness failure the finding is.
- CWE-331 (Insufficient Entropy) - a sibling under CWE-330: too few unpredictable bits in the result rather than the wrong algorithm producing them. Swapping in a CSPRNG and keeping a 6-digit output leaves that finding exactly where it was. MITRE records no direct relationship between the two.
- CWE-335 (Incorrect Usage of Seeds in Pseudo-Random Number Generator (PRNG)) - another sibling under CWE-330, for a generator seeded from a fixed or predictable value rather than one that is weak by algorithm.
- CWE-329 (Generation of Predictable IV with CBC Mode) - the CBC-IV case of a predictable value. MITRE places it under CWE-1204 (Generation of Weak Initialization Vector (IV)), a different branch of CWE-330's subtree, with no direct relationship to this page.
OWASP Classification
A04:2025 - Cryptographic Failures
Risk
High: An attacker who can predict a session or password-reset token takes over the account it belongs to, and a predicted key, IV or nonce breaks the encryption that depends on it.
Remediation Steps
Core Principle: Use a cryptographically secure PRNG for any security-sensitive randomness; never roll your own RNG.
Decide first whether this value is security-relevant
This CWE produces more false positives than most, because a weak generator is correct almost everywhere it appears. A detector can see the call; only you can see what the value becomes. The question is not "is this random enough" but "does guessing it get someone something".
- Usually not a finding, and closing it with that reason recorded is a legitimate outcome: shuffling a carousel or a set of recommendations, jitter on a retry backoff, sampling for a load test or a metrics sample, picking an A/B bucket, generating test fixtures, a cache-busting suffix, a temporary filename in a directory only the process can read.
- Usually a finding even when it does not look like one: session and remember-me identifiers, password-reset and email-verification tokens, CSRF tokens, API keys, OTP and 2FA codes, invitation and coupon codes with monetary value, "unguessable" share URLs, filenames written into a directory another user can reach, and every salt, IV, nonce and key.
- Three things move a borderline case toward "finding": the value is ever observable by someone who is not entitled to what it protects; guessing it grants access rather than merely revealing information; and it is long-lived, so an attacker gets more than one attempt.
- Ask what a duplicate would cost, separately from what a guess would cost. A GCM nonce needs uniqueness more urgently than unpredictability, and a deterministic generator restarted from the same seed - two replicas launched together, a container restored from a checkpoint - repeats its whole sequence.
Where the answer is "not security-relevant", the fix is a note on the finding, not a code change. Swapping a CSPRNG into a hot simulation loop costs throughput and buys nothing.
Locate the weak PRNG usage
- Identify which weak PRNG is in use, and pin the version: several of these have been fixed underneath the same call.
RandomStringUtils.randomAlphanumeric()isSecureRandom-backed from commons-lang3 3.15.0 andThreadLocalRandombefore it; Go's package-levelmath/randfunctions have drawn from a ChaCha8 generator since Go 1.22 andrand.Seedhas been a no-op since Go 1.24; .NET 6 movedSystem.Random's default generator to an OS-seeded xoshiro256**, so the classic "guess the tick count" seed attack no longer applies to it. A finding written against the old behaviour can be stale, and its recommended fix can be aimed at a line that is no longer the problem - Trace the data flow to understand what the random value is used for (session tokens, cryptographic keys, nonces, salts)
- Check for predictable seeding patterns (time-based seeds, PID-based seeds, static seeds), and note that an unpredictable seed does not rescue a statistical generator - the attacker recovers the state from the output and never needs the seed
Use cryptographically secure PRNGs (Primary Defense)
- Replace weak PRNGs with secure alternatives:
- Python: Use
secretsoros.urandom()instead ofrandom,random.shuffleandrandom.choiceincluded;random.SystemRandomis the one member of that module that draws from the OS - Java: Use
java.security.SecureRandominstead ofjava.util.Random,Math.random()orThreadLocalRandom - JavaScript/Node.js: Use
crypto.randomBytes(),crypto.randomInt()orcrypto.getRandomValues()instead ofMath.random()or anything derived fromDate.now() - .NET: Use
System.Security.Cryptography.RandomNumberGeneratorinstead ofSystem.RandomorRandom.Shared - PHP: Use
random_bytes(),random_int()orRandom\RandomizerwithRandom\Engine\Secureinstead ofrand(),mt_rand(),uniqid(),shuffle(),str_shuffle(),array_rand()orlcg_value() - Go: Use
crypto/randinstead ofmath/randormath/rand/v2
- Python: Use
- The dangerous names are rarely the obvious ones. A rule that greps for
rand(finds the calls a developer already suspects. The ones that survive review are the helpers whose names say nothing about randomness -str_shuffle(),array_rand(),RandomStringUtils.randomAlphanumeric(),ThreadLocalRandom.current(),Random.Shared,uniqid()- and the ones whose names suggest they were already fixed, such asmath/rand/v2. Search for the uses (token, secret, nonce, salt, code, key) as well as for the generators.
Avoid predictable seeding and validate randomness
- Do not seed by hand. A CSPRNG such as
SecureRandomorsecretsseeds itself from the OS entropy pool; a seed derived from the time, the PID or another predictable value gains nothing - Check the source and the size together: the value must come from a CSPRNG and be long enough for its job - 128 bits or more for tokens, 256 bits for many symmetric keys
- Salts, IVs and nonces carry their own length and uniqueness requirements on top of that; size each one for its use
Apply additional PRNG protections
- Generate a fresh value for every session, user and request; never reuse one
- Do not mix weak and strong sources, such as
randomalongsidesecrets; use the secure generator throughout - Encode tokens as base64url or hex to avoid character-encoding problems
Monitor and audit random number generation
- Log only metadata and failures for security-critical random generation; never log generated session IDs, API keys, tokens, salts, keys, IVs, or nonces
- Alert on a duplicate value where the design says duplicates cannot happen - a repeated session ID, nonce or reset token is a real signal that something has been reseeded, forked or restored from a snapshot
- Do not build monitoring around "entropy pool" levels. It is the folklore control for this CWE and it does not apply on any current system: Linux's
getrandom(2)blocks only once, at first boot, and never afterwards, and the/proc/sys/kernel/random/entropy_availfigure is not a budget that draws down. Nothing you would alert on can happen, and treating a value from/dev/urandomas suspect because a counter looked low is how a CSPRNG gets replaced with something worse
Test the PRNG fix
- Confirm the weak generator no longer feeds any security-relevant value, and that session tokens, keys and salts come from the CSPRNG at the intended length
- Check length, encoding, uniqueness handling and error paths; generate many values and confirm none repeat
- Do not read a passing statistical test as proof of cryptographic strength. Such tests can catch a badly broken implementation but cannot show that a generator is unpredictable
- Re-scan with the security scanner to confirm the finding is resolved
Common Vulnerable Patterns
- Using
randomorMath.randomfor cryptographic purposes - Seeding PRNGs with predictable values
// VULNERABLE - pseudo-code
key = weak_prng.getrandbits(128) // deterministic algorithm, not cryptographically secure
token = str(weak_prng.randint(0, 999999)) // ~20 bits of entropy, brute-forceable in milliseconds
Why this is vulnerable: Standard-library PRNGs like Python's random (Mersenne Twister/MT19937) or Java's java.util.Random (a linear congruential generator) are deterministic and designed for statistical simulation, not security. An attacker who observes enough consecutive outputs - from session tokens, password-reset links, API responses, or anywhere else a generated value is handed out - can reconstruct the generator's internal state and predict every future value it produces. MT19937 needs 624 consecutive 32-bit outputs, which a paginated list of generated identifiers can supply in a single response. If the seed is time-based or otherwise guessable, the entire output sequence is reproducible without even needing to observe prior outputs.
Secure Patterns
// SECURE - pseudo-code
key = csprng.random_bytes(16) // 128-bit key from the OS CSPRNG
token = csprng.url_safe_token(32) // 256-bit secure session/reset token
Why this works: A cryptographically secure PRNG (secrets/os.urandom in Python, SecureRandom in Java, crypto.randomBytes in Node.js, RandomNumberGenerator in .NET, random_bytes/random_int in PHP) draws from the operating system's entropy pool and is designed to resist state reconstruction from observed outputs. An attacker who has collected earlier tokens learns nothing that helps predict the next one, and at 128 and 256 bits guessing is not an option either.
Common Pitfalls
- Partial migration: Swapping the CSPRNG into the main token/session code path but leaving a related helper (a "shuffle," "sample," or legacy fallback function) still calling the weak PRNG - the vulnerability persists wherever that untouched call is still reachable, even if it looks unrelated to authentication.
- Manually seeding a "secure" RNG "just in case": Explicitly seeding a CSPRNG API with a derived value (timestamp, PID, hardcoded constant) instead of letting it draw from OS entropy by default. Most CSPRNG APIs don't need or want manual seeding; supplying a low-entropy seed can weaken the unpredictability guarantee depending on the implementation, and it never improves it.
- Switching the algorithm but not the output length: Replacing a weak PRNG call with a CSPRNG call but keeping the old value's short length or format (a 6-digit numeric code, a truncated byte string). A CSPRNG produces unpredictable bits, but a small output space is still brute-forceable regardless of what generated it.
- Deriving a token from a secure value mixed with a predictable one: Concatenating or hashing a real CSPRNG value together with a guessable component (username, timestamp, sequence counter) under the assumption that mixing "adds" strength. It does not, and the danger is not that the predictable part drags the rest down - guessing the username tells an attacker nothing about an independent 128-bit random value sitting beside it. The danger is that the predictable part contributes no entropy while making the token look strong: a 32-character value that is 8 random characters and 24 of username is an 8-character secret, and sizing it by total length hides that. Truncation or any lossy transformation is the other half, because it can discard unpredictable bits - a lossless re-encoding such as hex or Base64 does not, and trimming a predictable suffix costs nothing either. Size the secret by the random information it retains, not by the length it displays.
Language-Specific Guidance
For detailed examples and best practices in specific languages:
- C#/.NET - Using
System.Security.Cryptography.RandomNumberGenerator, avoidingSystem.Random - Go - Using
crypto/rand, Gin/Echo/Fiber examples - Java - Using
SecureRandom, Spring Boot/JAX-RS examples - JavaScript/Node.js - Using
crypto.randomBytes(), Express/Fastify/Next.js examples - PHP - Using
random_bytes(),random_int(), Laravel/Symfony/WordPress examples - Python - Using
secretsmodule,os.urandom(), Flask/Django/FastAPI examples