Skip to content

CWE-331: Insufficient Entropy - Java

Overview

Insufficient entropy is a question about the number of unpredictable bits in a value, not about which class produced it. In Java it shows up in three shapes. A seeded generator - new Random(System.currentTimeMillis()) or setSeed(...) - caps every value that follows at the range of the seed. An output that is simply too small - a 4-digit PIN, a 32-bit token, an 8-character identifier - falls to brute force whatever produced it. And a format with a fixed ceiling, such as a v4 UUID's 122 bits, cannot be raised by care taken anywhere else.

Primary Defence: Size the value against what it protects - at least 16 bytes for a token and 32 for key material - then fill it with java.security.SecureRandom, which seeds itself from OS entropy.

The related finding that the generator itself is not cryptographic - java.util.Random's 48-bit linear congruential state, Math.random(), ThreadLocalRandom - is CWE-338. The two are usually reported on the same line and both are covered below. java.util.Random fails on both counts at once: even seeded from a strong source it has only 48 bits of state, so nextLong() can return at most 2^48 of the 2^64 values its return type suggests.

Common Vulnerable Patterns

Using Random with time-based seed for tokens

import java.util.Random;

// VULNERABLE - Predictable token generation
public class InsecureTokenGenerator {
    private static final Random random = new Random();

    // VULNERABLE - Time-based seed
    public String generateSessionToken() {
        random.setSeed(System.currentTimeMillis());
        return Long.toHexString(random.nextLong());
    }
}

Why this is vulnerable: The output space is the seed space. Random.setSeed(long) keeps only the low 48 bits, and every value the instance produces afterwards is a function of them - so the session token here is one of however many millisecond values an attacker considers plausible, not one of the 2^64 a long could hold. A tester who knows the request happened within a given minute has 60,000 candidates; within a day, 86.4 million, which is seconds of work offline. This is a bound on top of the class's own limit rather than instead of it: even without setSeed, java.util.Random carries 48 bits of state, so nextLong() reaches at most 2^48 distinct values. Removing the setSeed line leaves a 48-bit token, which is still a CWE-331 finding.

Using Random for encryption keys

import java.util.Random;

// VULNERABLE - Using Random for encryption key
public class InsecureKeyGenerator {
    private static final Random random = new Random();

    public byte[] generateKey() {
        byte[] key = new byte[32];
        random.nextBytes(key);  // Predictable!
        return key;
    }
}

Why this is vulnerable:

  • Keys generated from Random are predictable.
  • Observed outputs can reveal future values.

Using Math.random() for API keys

// VULNERABLE - Math.random() for tokens
public String generateApiKey() {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < 32; i++) {
        sb.append((char)('A' + (int)(Math.random() * 26)));
    }
    return sb.toString();
}

Why this is vulnerable:

  • Math.random() uses the same predictable PRNG as Random.
  • API keys require CSPRNG-grade entropy.

Using UUID.randomUUID() as key material

import java.util.UUID;

// VULNERABLE - 122 bits, below the 128-bit minimum most key APIs require
public SecretKey deriveSigningKey() {
    byte[] material = UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8);
    return new SecretKeySpec(material, "HmacSHA256");
}

Why this is vulnerable: Not because the generator is weak - UUID.randomUUID() is specified to draw from "a cryptographically strong pseudo random number generator" and the JDK implements it over a shared SecureRandom. The problem is the ceiling. The method fills 16 bytes and then overwrites 4 bits with the version and 2 with the variant, leaving exactly 122 random bits in every UUID ever produced, and no care taken elsewhere raises that. 122 bits is comfortable for a session identifier or a reset token, and it sits below the 128-bit floor that key-derivation and signing routines commonly require of their inputs, so a UUID used as key material is a finding on a policy boundary rather than on a practical break. The sharper problem in the example above is that the string form is being hashed: 36 characters of lowercase hex and dashes, which is 122 bits of entropy dressed as 288 bits of input, and reading the array length is what makes it look sufficient.

For a token, prefer explicit SecureRandom bytes anyway - not because 122 bits is too few, but because the byte count is then a number you chose and can raise, rather than one the format fixed for you.

Insufficient entropy in PIN generation

import java.util.Random;

// VULNERABLE - Insufficient entropy (only 4 digits)
public int generatePin() {
    Random random = new Random();
    return random.nextInt(10000);  // 0000-9999
}

Why this is vulnerable:

  • 4 digits is only 10,000 possibilities.
  • Using Random makes the sequence guessable.

Weak sources that do not look like random number generators

The calls above announce themselves. These do not, which is why they survive review:

import org.apache.commons.lang3.RandomStringUtils;
import java.security.SecureRandom;
import java.util.concurrent.ThreadLocalRandom;

// VULNERABLE - ThreadLocalRandom is a performance optimisation, not a CSPRNG,
// and the range is under 20 bits regardless
int otp = ThreadLocalRandom.current().nextInt(100_000, 1_000_000);

// DEPENDS ON THE VERSION - commons-lang3 below 3.15.0 backs this with
// ThreadLocalRandom; 3.15.0 and later back it with SecureRandom
String apiKey = RandomStringUtils.randomAlphanumeric(32);

// SECURE - commons-lang3 3.16.0+. Explicit about the generator, and the
// replacement the 3.17.0 deprecation of randomAlphanumeric points you at
String apiKey = RandomStringUtils.secure().nextAlphanumeric(32);

// SECURE - the equivalent on 3.15.0 and earlier, where secure() does not
// exist. This overload takes the generator as an argument and is not
// deprecated, so it also compiles clean on current versions
String apiKey = RandomStringUtils.random(32, 0, 0, true, true, null, new SecureRandom());

Why this is vulnerable: ThreadLocalRandom exists to avoid contention on a shared Random instance under concurrency - it is faster, not stronger, and carries the same predictability. It is also the more honest half of this example, because the OTP above is under 20 bits whatever fills it.

RandomStringUtils.randomAlphanumeric() is the one worth reading the version number for, and it is the reason this entry cannot simply be called a vulnerable pattern. Nothing in the name suggests a random number generator at all, and up to and including commons-lang3 3.14.0 it drew from ThreadLocalRandom. 3.15.0 (July 2024) switched every static random* method to a SecureRandom, and its own class Javadoc records the change: "Before version 3.15.0, this class used ThreadLocalRandom.current(), which was NOT cryptographically secure." 3.16.0 (August 2024) added secure() and insecure(), and 3.17.0 added secureStrong() and deprecated the static random* methods in favour of the three factories. Verified against the published sources: 3.15.0 has no secure() factory, 3.16.0 has secure() and insecure() and no deprecation on randomAlphanumeric, 3.17.0 adds secureStrong() and the deprecations, and on 3.19.0 RandomStringUtils.randomAlphanumeric(32) delegates to secure().nextAlphanumeric(32) and is backed by a Hash_DRBG instance.

So a scanner hit on this line is a real finding on an old dependency and a false positive on a current one - check mvn dependency:tree before changing code, and prefer upgrading the library to rewriting the call. Where the call stays, name the generator explicitly, as in the two secure forms above. Either way 32 characters from the 62-character alphanumeric set is 190 bits, well above the 128-bit floor; the entropy was never the problem here, the generator behind it was.

Secure Patterns

Using SecureRandom

import java.security.SecureRandom;
import java.util.Base64;
import java.util.HexFormat;

public class SecureTokenGenerator {
    // SECURE - Use SecureRandom (thread-safe, automatically seeded)
    private static final SecureRandom secureRandom = new SecureRandom();

    // Generate cryptographically secure session token (128+ bits)
    public static String generateSessionToken() {
        byte[] token = new byte[16];  // 128 bits
        secureRandom.nextBytes(token);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(token);
    }

    // Generate hex-encoded token
    public static String generateHexToken(int bytes) {
        byte[] token = new byte[bytes];
        secureRandom.nextBytes(token);
        // Java 8/11: replace with a small toHex helper.
        return HexFormat.of().formatHex(token);
    }

    // Generate CSRF token (256 bits)
    public static String generateCsrfToken() {
        byte[] token = new byte[32];  // 256 bits
        secureRandom.nextBytes(token);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(token);
    }

    // Generate API key (384 bits)
    public static String generateApiKey() {
        byte[] key = new byte[48];  // 384 bits
        secureRandom.nextBytes(key);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(key);
    }

    // Generate password reset token (256 bits)
    public static String generatePasswordResetToken() {
        byte[] token = new byte[32];  // 256 bits
        secureRandom.nextBytes(token);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(token);
    }

    // One-time code delivered out of band. NOT a token: 6 digits is
    // 6 * log2(10) = 19.9 bits, the same entropy the 4-digit PIN above is
    // marked VULNERABLE for, only 100x larger. What makes this usable is
    // the attempt limit, the short expiry and single-use enforcement at the
    // call site - not the generator. If those three are not in place,
    // lengthening the code is not the fix either.
    public static String generateOtp(int length) {
        StringBuilder code = new StringBuilder();
        for (int i = 0; i < length; i++) {
            // nextInt(10) rejects and retries; a byte % 10 would not, because
            // 256 is not a multiple of 10 (0-5 appear 26 times per 256, 6-9
            // appear 25).
            code.append(secureRandom.nextInt(10));
        }
        return code.toString();
    }

    // Generate cryptographic key (256 bits for AES-256)
    public static byte[] generateEncryptionKey(int keySize) {
        byte[] key = new byte[keySize / 8];
        secureRandom.nextBytes(key);
        return key;
    }

    // Generate IV for AES encryption
    public static byte[] generateIV() {
        byte[] iv = new byte[16];  // 128 bits for AES
        secureRandom.nextBytes(iv);
        return iv;
    }
}

Why this works:

  • SecureRandom is a CSPRNG seeded from OS entropy sources.
  • Output is not predictably reversible like LCG-based Random.
  • Token sizes (128+ bits) make guessing infeasible.

Complete encryption example with secure randomness

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Base64;

public class SecureEncryption {
    private static final SecureRandom secureRandom = new SecureRandom();
    private static final int GCM_NONCE_LENGTH = 12;  // 96 bits
    private static final int GCM_TAG_LENGTH = 128;   // 128 bits

    // Generate AES-256 key using SecureRandom
    public static SecretKey generateAESKey() throws Exception {
        KeyGenerator keyGen = KeyGenerator.getInstance("AES");
        keyGen.init(256, secureRandom);  // Use SecureRandom for key generation
        return keyGen.generateKey();
    }

    // Generate secure nonce for GCM mode
    public static byte[] generateNonce() {
        byte[] nonce = new byte[GCM_NONCE_LENGTH];
        secureRandom.nextBytes(nonce);
        return nonce;
    }

    // Encrypt with AES-GCM (authenticated encryption)
    public static EncryptedData encrypt(byte[] plaintext, SecretKey key) throws Exception {
        // Generate secure nonce
        byte[] nonce = generateNonce();

        // Create cipher
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, nonce);
        cipher.init(Cipher.ENCRYPT_MODE, key, spec, secureRandom);

        byte[] ciphertext = cipher.doFinal(plaintext);

        return new EncryptedData(nonce, ciphertext);
    }

    // Decrypt AES-GCM ciphertext
    public static byte[] decrypt(EncryptedData encrypted, SecretKey key) throws Exception {
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, encrypted.nonce);
        cipher.init(Cipher.DECRYPT_MODE, key, spec);

        return cipher.doFinal(encrypted.ciphertext);
    }

    // Data class to hold nonce and ciphertext
    public static class EncryptedData {
        public final byte[] nonce;
        public final byte[] ciphertext;

        public EncryptedData(byte[] nonce, byte[] ciphertext) {
            this.nonce = nonce;
            this.ciphertext = ciphertext;
        }
    }
}

Why this works:

  • Keys and nonces come from SecureRandom, not predictable PRNGs.
  • GCM provides confidentiality and integrity with a 128-bit tag.
  • Nonce size and randomness avoid reuse risk.
// Usage example
SecretKey key = SecureEncryption.generateAESKey();
byte[] plaintext = "sensitive data".getBytes();
EncryptedData encrypted = SecureEncryption.encrypt(plaintext, key);
byte[] decrypted = SecureEncryption.decrypt(encrypted, key);

SecureRandom algorithm selection

import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

public class SecureRandomConfig {
    // Get strongest available SecureRandom instance
    public static SecureRandom getStrongSecureRandom() {
        try {
            // Try to get the strongest algorithm
            return SecureRandom.getInstanceStrong();
        } catch (NoSuchAlgorithmException e) {
            // Fallback to default (still secure)
            return new SecureRandom();
        }
    }

    // Request-path default. Do NOT name an algorithm here:
    // - "NativePRNG"       reads /dev/urandom on Unix; fine, but no better
    //                      than the default and unavailable on Windows
    // - "SHA1PRNG"         cross-platform, and setSeed() REPLACES its seed
    //                      before first use, which makes it deterministic
    // - no argument        DRBG on a current JDK, never blocks after boot
    public static SecureRandom getRequestPathSecureRandom() {
        return new SecureRandom();
    }
}

Why this works:

  • new SecureRandom() resolves to the highest-priority registered implementation - DRBG on a current JDK - which is seeded from the OS and needs nothing from the caller. Naming an algorithm can only narrow that, and the algorithm most often named by hand is the one to avoid.
  • getInstanceStrong() selects whatever the JDK's securerandom.strongAlgorithms property lists, which is where a deployment expresses a policy about which source is acceptable for long-lived keys. Reading it from configuration is the point; hard-coding the same name in application code defeats it.
  • The fallback to new SecureRandom() is safe rather than a silent downgrade: NoSuchAlgorithmException here means the configured strong algorithm is not registered, and the default remains a CSPRNG.

Framework-Specific Guidance

Spring Security - Session Management

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import java.security.SecureRandom;

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf
                // Spring Security uses SecureRandom internally for CSRF tokens
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            )
            .sessionManagement(session -> session
                // Spring Session uses SecureRandom for session IDs automatically
                .sessionFixation().newSession()
            );
        return http.build();
    }

    // Custom token generation in Spring
    @Bean
    public SecureRandom secureRandom() {
        return new SecureRandom();
    }
}

// Generate custom tokens in Spring components
import org.springframework.stereotype.Service;
import java.security.SecureRandom;
import java.util.Base64;

@Service
public class TokenService {
    private final SecureRandom secureRandom;

    public TokenService(SecureRandom secureRandom) {
        this.secureRandom = secureRandom;
    }

    public String generateVerificationToken() {
        byte[] token = new byte[32];
        secureRandom.nextBytes(token);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(token);
    }
}

JWT (JSON Web Token) Security

import io.jsonwebtoken.Jwts;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Date;
import javax.crypto.SecretKey;

public class JwtTokenProvider {
    private static final SecureRandom secureRandom = new SecureRandom();

    // SECURE - Jwts.SIG.HS256.key() generates a key of the size the algorithm
    // requires: 256 bits for HS256, drawn from SecureRandom. Loading it from
    // configuration rather than generating it here is what a deployment does -
    // a key created at class-init is a new key on every restart and a
    // different key in every replica, so every token issued before the last
    // restart stops verifying.
    private final SecretKey signingKey;

    public JwtTokenProvider(SecretKey signingKey) {
        this.signingKey = signingKey;
    }

    // Use once to mint the key that then goes into the secret store.
    public static SecretKey generateSigningKey() {
        return Jwts.SIG.HS256.key().build();
    }

    public String generateJwtToken(String userId) {
        // Generate unique JTI (JWT ID) using SecureRandom - 128 bits, which is
        // about uniqueness and replay tracking rather than secrecy, since the
        // jti travels in the token's payload in cleartext
        byte[] jtiBytes = new byte[16];
        secureRandom.nextBytes(jtiBytes);
        String jti = Base64.getUrlEncoder().withoutPadding().encodeToString(jtiBytes);

        return Jwts.builder()
            .subject(userId)
            .id(jti)
            .issuedAt(new Date())
            .expiration(new Date(System.currentTimeMillis() + 3_600_000))
            .signWith(signingKey)
            .compact();
    }
}

Why this works: Jwts.SIG.HS256.key().build() is the current jjwt API and picks the key size from the algorithm, so there is no length to get wrong - verified as 256-bit HmacSHA256 on jjwt 0.13.0. The forms this replaces (Keys.secretKeyFor(SignatureAlgorithm.HS256), setSubject, setId, setIssuedAt, setExpiration) all still exist and all emit deprecation warnings from 0.12.0 onward; SignatureAlgorithm itself is deprecated as a type. If you are on 0.11.x or earlier, Keys.secretKeyFor(SignatureAlgorithm.HS256) is the equivalent and produces the same 256 bits - the entropy was never the difference between them.

Considerations

Not every random value is a secret. Jitter on a retry backoff, a sample of records for a report, a shuffled result set - none of these gain an attacker anything if guessed, and java.util.Random is the right class for them. The question is not "is this random" but "does guessing it get someone something". Session IDs, reset tokens, API keys, CSRF tokens, OTPs, salts, IVs and key material all fail that test. If the value is not one of those, record the finding as a false positive with the reason.

Length is a separate decision from class. A 4-byte value from SecureRandom is still only 32 bits. Use at least 16 bytes for tokens and 32 for key material, and encode with Base64.getUrlEncoder().withoutPadding() for anything that travels in a URL, so the value is not mangled in transit and then "fixed" by truncation.

getInstanceStrong() is not simply the better choice, and the reason usually given for that is out of date. The JDK's securerandom.strongAlgorithms property is Windows-PRNG:SunMSCAPI,DRBG:SUN on Windows and NativePRNGBlocking:SUN,DRBG:SUN everywhere else, and NativePRNGBlocking reads /dev/random. The old advice - that this stalls a busy service because the kernel runs out of entropy - has not been true since Linux 5.6 (March 2020): random(4) states that /dev/random "will no longer block except during early boot process", and no read on either device consults an entropy estimate afterwards. Entropy is not consumed by being read.

What survives is narrow and real: before the kernel CRNG is first seeded, /dev/random does block. That is early boot on an embedded board with no hardware RNG, a container started immediately after a freshly-cloned VM image comes up, or a minimal CI image with little device activity - and a service that calls getInstanceStrong() in a static initialiser can hang there rather than start. Plain new SecureRandom() avoids it (it is DRBG on a current JDK and never blocks after initialisation), and is the right default for request-path work. Reserve getInstanceStrong() for long-lived key generation, and if it is on a startup path, make sure the platform seeds its pool early. Do not respond to a stall by reverting to java.util.Random; the failure mode you would be trading it for is far worse than a slow start.

UUID.randomUUID() is CSPRNG-backed and fixed at 122 bits. The Javadoc specifies "a cryptographically strong pseudo random number generator" and the JDK implements it over a shared SecureRandom, so the generator is not the question. The ceiling is: 4 bits of version and 2 of variant are overwritten, so 122 of the 128 are random and nothing raises that. Fine for a session identifier or a reset token, below the 128-bit input minimum several key APIs enforce.

setSeed() replaces the seed on some providers and supplements it on others, and the difference decides whether a SecureRandom is deterministic. Measured on JDK 26: two SecureRandom.getInstance("SHA1PRNG") instances given the same setSeed(...) bytes before any output is drawn produce byte-identical streams. The self-seeding is skipped entirely, so the generator's whole output space collapses to whatever the caller passed - which is CWE-331's core failure mode wearing a SecureRandom label, and it is how SecureRandom.getInstance("SHA1PRNG") came to be used as a key-derivation function in code that has since been broken publicly. The same test against the default provider (DRBG) gives different streams, because there setSeed() mixes into existing state. Call setSeed() after output has already been drawn and even SHA1PRNG supplements.

The rules that follow: never call setSeed() on the request path, never pass it anything derived from a password, an ID or a timestamp, and do not select "SHA1PRNG" by name. If tests need reproducible values, inject a substitutable generator into the class under test rather than seeding the real one.

Common Pitfalls

  • Falling back to a non-cryptographic generator under load: Creating a fresh SecureRandom per request avoids shared-state issues but can be slow, so it's tempting to "optimize" a hot path with ThreadLocalRandom.current() for token bytes - this silently reintroduces a predictable generator. Reuse a single SecureRandom instance instead.
  • Truncating a correctly-generated key/token to fit a legacy column: Shrinking a SecureRandom-generated byte array or its hex/base64 encoding to fit a fixed-width VARCHAR column reduces the entropy below the intended minimum even though the generator itself is correct - widen the column or re-encode more compactly rather than chopping raw bytes.

Additional Resources