CWE-331: Insufficient Entropy
Overview
Insufficient entropy is about how much randomness a value carries, not about which function produced it. A value has insufficient entropy when the set of values it could have taken is small enough to enumerate: too few bits were requested, the bits were lost after generation by truncation or by rendering through a small alphabet, or the generator was seeded from something far smaller than its output suggests. A 32-bit token drawn from a perfect OS CSPRNG is a CWE-331 finding. So is a generator with a 2^19937 period seeded from time(), because the output space collapses to the seed space - the 86,400 second-values in a day the attacker already knows the request fell in - no matter how good the algorithm downstream of the seed is.
Two neighbouring weaknesses are usually reported alongside it and are worth separating, because they have different fixes:
- CWE-338 - the generator is not cryptographic: Mersenne Twister, a linear congruential generator,
Math.random(). The defect there is that observed outputs reveal the internal state, and it is fixed by changing which function you call. - CWE-330 - the parent class covering both.
CWE-331 is fixed by increasing the number of unpredictable bits in the result. The two overlap in practice, because a weak generator is usually also seeded from a small space, but switching to a CSPRNG and keeping the old 6-digit output leaves the CWE-331 finding exactly where it was. MITRE's own mitigation for CWE-331 is a single sentence: increase the number of bits in keys and seeds.
OWASP Classification
A04:2025 - Cryptographic Failures
Risk
High: Weak entropy lets an attacker brute-force or predict values that are supposed to be unguessable: encryption keys, session tokens, CSRF tokens, password reset tokens, API keys, IVs/nonces, and random salts.
Remediation Steps
Core Principle: Draw every secret - token, key, nonce - from the OS CSPRNG with enough bits, and do not narrow it through a seed, a small alphabet, or truncation.
Locate the insufficient entropy source
- Start at the line the finding points to, then count the bits the value actually carries, which is not the same as its length on screen: a 16-character hexadecimal string is 64 bits, and a 16-character numeric code is
16 * log2(10) ~= 53bits - Find the narrowest point in the path. Entropy is capped by the smallest stage - the seed, the number of bytes requested, the output alphabet, and any truncation afterwards - so the fix belongs at whichever of those is lowest, not necessarily at the generator
- Determine what the random values are used for: encryption keys, IVs/nonces, session tokens, CSRF tokens, password reset tokens, API keys
- Establish how many guesses an attacker gets. An offline target (an encryption key, a signed token an attacker holds) is limited only by hardware; an online one is limited by rate limiting and lockout, which is the difference between a 6-digit OTP being acceptable and being an account takeover
Request enough bits (Primary Defense)
- The floor is 128 bits for anything an attacker gains by guessing. Below that, "how long would this take to brute-force" becomes a question about the attacker's budget rather than a settled no
- Session tokens: 128+ bits (16+ bytes) minimum
- CSRF tokens: 128+ bits minimum
- Encryption keys: Match the algorithm's key size (256 bits for AES-256, 128 bits for AES-128 - AES-128 is not a weak key size, it is a smaller margin)
- Password reset tokens and API keys: 256 bits is the usual choice. The extra bits over the 128-bit floor cost nothing, and these are long-lived values that end up in logs, referrers, and support tickets
- One-time codes delivered out of band: a 6-digit code carries
6 * log2(10) ~= 19.9bits and is only defensible behind a strict attempt limit, a short expiry, and single use. Write those three controls down as part of the fix, because without them the code is the whole authenticator - Formats with a fixed ceiling: a v4 UUID carries 122 random bits regardless of the generator behind it, because 6 of its 128 bits are fixed version and variant markers. That is enough for an identifier and below the usual policy minimum for key material
Use a cryptographically secure RNG
- Use the OS-provided cryptographic RNG:
- getrandom()/
/dev/urandomor equivalent OS CSPRNG APIs (Linux/Unix) - CNG or equivalent OS CSPRNG APIs (Windows)
- SecureRandom (Java)
- secrets module (Python 3.6+)
- crypto.randomBytes (Node.js)
- RandomNumberGenerator (C#; RNGCryptoServiceProvider is obsolete from .NET 6 onward and applies only to .NET Framework and .NET Core below 6)
- getrandom()/
- Do not use weak or predictable sources:
- Math.random() (JavaScript)
- rand(), random() (C/C++)
- Random() (Python)
- new Random() (Java)
- time() as seed
Do not narrow the seed
Explicit seeding is CWE-331's characteristic sink, and "use a CSPRNG" does not cover it. A generator's output space is capped by its seed space. Seed MT19937 - period 2^19937 - with time(), and the sequence it produces is one of the roughly 86,400 sequences available for a known day, whatever the period says. The same applies to a PID (typically under 2^22), a user ID, an incrementing counter, or a hash of a handful of environment values.
- Never seed from time(), a PID, an object ID, or a counter. The output collapses to that value's range
- Do not seed at all where the API seeds itself.
SecureRandom,secrets,crypto.randomBytes,random_bytesandRandomNumberGeneratorall draw from the OS pool; there is nothing to add - Watch for a CSPRNG re-seeded from a weak source. Some APIs treat an explicit seed as a supplement and some treat it as a replacement, and where it replaces, the CSPRNG becomes as deterministic as its seed. Java's
SecureRandomdoes both depending on the algorithm - see the Java page - A wish for reproducible values means the generator belongs behind an interface the test can substitute, not that seeding should be reintroduced on the production path
Confirm the fix
- Assert on the bit count, not the string length: generate a value and check the byte count going in and the alphabet coming out
- Assert the legitimate path still works. Widening a token usually means widening a database column, a cookie, or a URL; a token that is silently truncated on write is back where it started
- Where a value was shortened or re-encoded to fit somewhere, find that constraint and confirm it was widened rather than the value re-truncated
- Statistical tests can catch a severe implementation mistake but cannot prove unpredictability, and a uniform distribution says nothing about the size of the space it is uniform over. Review the source, the seed handling, and the output length instead
- Re-scan with the security scanner to confirm the issue is resolved
Common Vulnerable Patterns
- A seed drawn from a small space:
srand(time(NULL)),new Random(System.currentTimeMillis()),mt_srand(time()),random.seed(int(time.time())), or MITRE's own example,srand($userID). However long the generator's period, the result is one of the few sequences an attacker can enumerate - Too few bits requested: a 32-bit token, a 4-digit PIN, an 8-character hex identifier
- Entropy lost after generation: truncating a strong value to fit a fixed-width column or shorten a URL, or rendering it through a small alphabet - 16 characters drawn from 10 digits is about 53 bits, not the 128 the byte count suggested
- A timestamp or counter where an unpredictable value is required, such as a CBC IV
- A format with a fixed ceiling used as key material, such as a v4 UUID's 122 bits
- A generator swapped without resizing the output: the CSPRNG is now correct and the 6-digit code it feeds is unchanged
Common Pitfalls
- CSPRNG key, reused nonce/IV: Switching key generation to a CSPRNG fixes one half of an encryption call, but if the nonce/IV is generated once and cached or reused across multiple encryptions under the same key (a static per-session value instead of per-message), the entropy source no longer matters - nonce reuse breaks AEAD modes like GCM regardless of how the nonce was originally generated.
- Right byte count, wrong output alphabet: Generating a value from a source sized for "128 bits," then rendering it through a small alphabet - e.g., 16 characters from a 10-digit numeric set gives roughly
16 * log2(10) ~= 53 bits, far short of the intended 128, even though the byte count going in looked correct. - One strong seed, many weak derivations: Generating a single random value with a CSPRNG, then deriving a session token, CSRF token, and API key from it by concatenation or a fast non-cryptographic hash instead of independent CSPRNG calls or a proper KDF - correlating one derived value can reveal information about the others.
- UUIDs treated as full-entropy secrets: Using a v4 UUID as key material because "it's random" - exactly 122 of its 128 bits are random, the other 6 being fixed version and variant markers, and nothing done elsewhere raises that ceiling. The generator is usually not the problem on a current runtime: Java's
UUID.randomUUID(), .NET 6+'sGuid.NewGuid(), Node'scrypto.randomUUID()and Python'suuid.uuid4()are all CSPRNG-backed. 122 bits is comfortable for an identifier or a session token but sits below the 128-bit minimum that many key-derivation and signing routines require of their inputs, which is the reason to reach for a byte generator instead.
Language-Specific Guidance
Worked examples for each language:
- Python - the secrets module and os.urandom
- Java - SecureRandom
- JavaScript/Node.js - crypto.randomBytes and crypto.getRandomValues
- C# - RandomNumberGenerator.GetBytes() on .NET 6+
- PHP - random_bytes and random_int