CWE-331: Insufficient Entropy - Java
Overview
Insufficient entropy in Java occurs when using java.util.Random or Math.random() instead of java.security.SecureRandom for cryptographic operations. The Random class is designed for statistical simulations, not security, and produces predictable sequences that can be reproduced if the seed is known.
Primary Defence: Use java.security.SecureRandom for all security-sensitive random value generation including session tokens, encryption keys, and initialization vectors.
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:
Randomis deterministic; outputs are predictable from its 48-bit seed.System.currentTimeMillis()makes the seed guessable.
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() for security tokens
import java.util.UUID;
// RISKY - UUIDs are for identifiers, not secrets
public String generateResetToken() {
return UUID.randomUUID().toString(); // Avoid for security tokens
}
Why this is vulnerable:
- UUIDs are identifiers, not security tokens; entropy and use are implementation-dependent.
- Password reset tokens should use explicit
SecureRandomwith sufficient length.
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.
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);
}
// Generate numeric PIN with sufficient entropy
public static String generateSecurePin(int length) {
if (length < 6) {
throw new IllegalArgumentException("PIN must be at least 6 digits");
}
StringBuilder pin = new StringBuilder();
for (int i = 0; i < length; i++) {
pin.append(secureRandom.nextInt(10));
}
return pin.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();
}
}
// Get specific algorithm (platform-dependent)
public static SecureRandom getSecureRandom(String algorithm) {
try {
// Common algorithms:
// - "NativePRNG" (Unix/Linux - uses /dev/urandom)
// - "Windows-PRNG" (Windows - uses CryptGenRandom)
// - "SHA1PRNG" (Cross-platform, but avoid if possible)
return SecureRandom.getInstance(algorithm);
} catch (NoSuchAlgorithmException e) {
return new SecureRandom(); // Fallback to default
}
}
}
Why this works:
getInstanceStrong()selects the strongest configured CSPRNG.- Fallback
new SecureRandom()remains cryptographically secure. - Avoids weaker or legacy PRNG choices when possible.
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 io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.security.SecureRandom;
public class JwtTokenProvider {
// SECURE - Generate signing key with SecureRandom
private static final Key SIGNING_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);
private static final SecureRandom secureRandom = new SecureRandom();
public String generateJwtToken(String userId) {
// Generate unique JTI (JWT ID) using SecureRandom
byte[] jtiBytes = new byte[16];
secureRandom.nextBytes(jtiBytes);
String jti = Base64.getUrlEncoder().withoutPadding().encodeToString(jtiBytes);
return Jwts.builder()
.setSubject(userId)
.setId(jti) // Unique JWT ID
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 3600000))
.signWith(SIGNING_KEY)
.compact();
}
}
Verification
After implementing the recommended secure patterns, verify the fix through multiple approaches:
- Manual testing: Submit malicious payloads relevant to this vulnerability and confirm they're handled safely without executing unintended operations
- Code review: Confirm all instances use the secure pattern (parameterized queries, safe APIs, proper encoding) with no string concatenation or unsafe operations
- Static analysis: Use security scanners to verify no new vulnerabilities exist and the original finding is resolved
- Regression testing: Ensure legitimate user inputs and application workflows continue to function correctly
- Edge case validation: Test with special characters, boundary conditions, and unusual inputs to verify proper handling
- Framework verification: If using a framework or library, confirm the recommended APIs are used correctly according to documentation
- Authentication/session testing: Verify security controls remain effective and cannot be bypassed (if applicable to the vulnerability type)
- Rescan: Run the security scanner again to confirm the finding is resolved and no new issues were introduced