Skip to content

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) ~= 53 bits
  • 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.9 bits 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/urandom or 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)
  • 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_bytes and RandomNumberGenerator all 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 SecureRandom does 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+'s Guid.NewGuid(), Node's crypto.randomUUID() and Python's uuid.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

Additional Resources