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
Randomare 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 asRandom.- 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
Randommakes 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:
SecureRandomis 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 -DRBGon 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'ssecurerandom.strongAlgorithmsproperty 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:NoSuchAlgorithmExceptionhere 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
SecureRandomper request avoids shared-state issues but can be slow, so it's tempting to "optimize" a hot path withThreadLocalRandom.current()for token bytes - this silently reintroduces a predictable generator. Reuse a singleSecureRandominstance 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-widthVARCHARcolumn 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
- CWE-331: Insufficient Entropy
- Java Cryptography Architecture
- Java SecureRandom Documentation
- OWASP Cryptographic Storage Cheat Sheet
- Apache Commons Lang RandomStringUtils - the source for the 3.15.0 generator change and the 3.16.0
secure()deprecation noted above - random(4) - Linux manual page - the source for
/dev/randomno longer blocking after early boot since Linux 5.6