CWE-330: Use of Insufficiently Random Values - Java
Overview
java.util.Random and Math.random() are pseudo-random number generators (PRNGs) built on a 48-bit linear congruential generator. The seed is not the interesting part - new Random() mixes System.nanoTime() with an internal uniquifier, so guessing it is awkward - and guessing it is unnecessary. The state is small enough and the algorithm linear enough that the output gives it away directly: measured on JDK 26, a single nextLong() value narrows the 48-bit state to 2^16 candidates, one brute-force loop of 65,536 iterations picks the right one, and every subsequent value is then predicted exactly.
This makes these classes unsuitable for security-sensitive values: session tokens, password reset links, API keys, OTP codes, CSRF tokens, or cryptographic nonces. For these purposes, the correct class is java.security.SecureRandom, which draws from the OS cryptographic random source through the configured JCA provider - on JDK 26 that is DRBG from the SUN provider by default, seeded from securerandom.source.
Primary Defence: Replace all new Random() and Math.random() in security contexts with SecureRandom. A single shared SecureRandom instance is thread-safe and can be reused across the application.
Common Vulnerable Patterns
Token Generation with java.util.Random
import java.util.Random;
// VULNERABLE - java.util.Random produces predictable sequences
public class TokenService {
private static final Random RANDOM = new Random();
public String generatePasswordResetToken() {
long token = RANDOM.nextLong();
return Long.toHexString(token);
}
}
Why this is vulnerable:
Randomuses a 48-bit linear congruential generator, andnextLong()publishes 64 bits of it in one go. One output - a reset token requested for the attacker's own account - is enough: the top 32 bits are the state's top 32 bits, the remaining 16 fall out of a 65,536-iteration loop, and every token issued after it is then computed rather than guessed. Verified against JDK 26. A sharedstatic final Randommakes this worse, because "after it" means every account, not just the attacker's.
OTP with Math.random()
// VULNERABLE - Math.random() uses a shared java.util.Random instance
public int generateOtp() {
return (int) (Math.random() * 900_000) + 100_000;
}
Why this is vulnerable:
Math.random()is backed byjava.util.Random. An attacker who can observe a sequence of OTP values can predict the next one, allowing them to bypass SMS-based second factors.
Session ID from Timestamp + Random
import java.util.HexFormat;
import java.util.Random;
// VULNERABLE - seeding from timestamp makes the seed guessable
public String generateSessionId() {
Random rng = new Random(System.currentTimeMillis()); // predictable seed
byte[] bytes = new byte[16];
rng.nextBytes(bytes);
return HexFormat.of().formatHex(bytes);
}
Why this is vulnerable:
- Seeding
RandomfromSystem.currentTimeMillis()makes the seed knowable to anyone who can observe the approximate time of the request. An attacker can enumerate timestamps within a small window to find the seed.
setSeed() on a Freshly Created SHA1PRNG
import java.security.SecureRandom;
// VULNERABLE - on SHA1PRNG, setSeed() before first use REPLACES the seed
public byte[] deriveTokenBytes(long tenantId) throws Exception {
SecureRandom rng = SecureRandom.getInstance("SHA1PRNG");
rng.setSeed(tenantId);
byte[] bytes = new byte[16];
rng.nextBytes(bytes);
return bytes;
}
Why this is vulnerable:
setSeed()does not mean the same thing on every provider, and the difference is invisible at the call site. On the default provider it supplements the existing seeding, so the generator stays unpredictable no matter what you pass it. OnSHA1PRNGit replaces the seeding entirely if the instance has not produced output yet - which it has not, one line aftergetInstance. Measured on JDK 26: two freshSHA1PRNGinstances givensetSeed(42L)produced byte-identical streams (d50ac288b90ede2e...), while two default-provider instances given the same value did not. The class name saysSecureRandomand the algorithm is not the problem; the seed is, and every tenant's token is now derived from a number the tenant knows. Name no algorithm at all -new SecureRandom()- and never callsetSeed()before drawing from a generator you want to be unpredictable.
Secure Patterns
256-bit URL-Safe Token
import java.security.SecureRandom;
import java.util.Base64;
public class TokenGenerator {
// SECURE - SecureRandom is thread-safe; reuse the instance
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
public static String generateToken() {
byte[] bytes = new byte[32]; // 256 bits
SECURE_RANDOM.nextBytes(bytes);
// URL-safe Base64, no padding - safe to use in URLs and headers
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
}
}
Why this works:
SecureRandomseeds itself from the OS cryptographic random source on first use and never needs seeding by the caller. Its output cannot be predicted without the kernel's internal state, which no application-level attacker reaches. 32 bytes encode to a 43-character URL-safe string with padding stripped, so it drops into a path segment or anAuthorizationheader unescaped.
6-Digit OTP
import java.security.SecureRandom;
public class OtpGenerator {
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
public static int generateOtp() {
// SECURE - generates a 6-digit code in [100000, 999999]
return SECURE_RANDOM.nextInt(900_000) + 100_000;
}
}
Why this works:
SecureRandom.nextInt(bound)uses the CSPRNG to generate an unbiased value in[0, bound). Adding100_000shifts the range to[100000, 999999]for a 6-digit OTP.
API Key Generation
import java.security.SecureRandom;
import java.util.HexFormat;
public class ApiKeyGenerator {
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
public static String generateApiKey() {
byte[] keyBytes = new byte[32]; // 256-bit key
SECURE_RANDOM.nextBytes(keyBytes);
return HexFormat.of().formatHex(keyBytes); // 64-char hex string
}
// Alternative: the algorithm named by the securerandom.strongAlgorithms
// security property, for deployments with a policy about which one to use
public static String generateMasterKey() throws java.security.NoSuchAlgorithmException {
SecureRandom strongRng = SecureRandom.getInstanceStrong();
byte[] keyBytes = new byte[32];
strongRng.nextBytes(keyBytes);
return HexFormat.of().formatHex(keyBytes);
}
}
Why this works:
HexFormat.of().formatHex()(Java 17+) is the standard library's hex encoder, 64 characters for a 32-byte key.new SecureRandom()is already sufficient for keys.getInstanceStrong()does not return a "more random" generator - it returns whichever algorithm thesecurerandom.strongAlgorithmssecurity property names first, which is a deployment policy setting, not a strength ranking. Both draw from the same kernel CSPRNG. Measured on JDK 26/Windows, the default resolved toDRBGfromSUNandgetInstanceStrong()toWindows-PRNGfromSunMSCAPI, and the "strong" one was consistently the faster of the two over 1,000 32-byte draws - by about twice once the JIT had fully warmed both up, and by more than an order of magnitude on the first thousand draws. Which one wins is a property of the platform's providers, not of how strong they are; do not reach forgetInstanceStrong()believing the default is weaker, and use it where a policy requires a named algorithm.
Considerations
Ask what guessing the value would get someone. Randomness has non-security uses everywhere - sampling, shuffling, jitter, cache-busting, test fixtures - and none of them need a CSPRNG. The finding is material when the value is a session identifier, a token, a key, an OTP, a salt, an IV, or anything else whose unpredictability is what makes it work. If it is not, the general-purpose generator is the correct choice and the finding should be closed with the reason recorded.
Do not blanket-replace. A cryptographic generator draws on the OS entropy pool and is meaningfully slower than a PRNG. That cost is irrelevant for a handful of tokens per request and very relevant in a simulation or a rendering loop generating millions of values. Replacing every call site to make a scanner quiet trades real throughput for no security benefit, and it makes the genuine findings harder to see.
UUID.randomUUID() is CSPRNG-backed but fixed at 122 bits. Reasonable for a
session identifier, short of what you want for key material, and the constraint
is the UUID format rather than the generator.
A shared SecureRandom instance is fine. It is thread-safe, and creating one
per request adds seeding cost for no benefit - a private static final field is
the usual shape.
Anything derived from a weak value stays weak. Hashing it, base64-encoding it, concatenating a timestamp, or truncating it changes how the output looks without adding entropy - the result is still fully determined by the predictable input. There is no post-processing that fixes the source; only replacing the generator does.
Check the length once the generator is right. This CWE is about the unpredictability of the value, which depends on both the source and how much of it you take. Four bytes from a cryptographic generator is still only 32 bits. Use at least 16 bytes for tokens and 32 for key material, and remember hex encoding doubles the character count, which is where half the intended entropy usually goes missing.
Testing
- Normal input: exercise token, OTP, API-key, and session creation flows that now call
SecureRandom. - Boundary input: test minimum and maximum token lengths, encoding into URLs or headers, and concurrent generation under load.
- Malicious input: generate many values around the same timestamp and confirm there is no deterministic sequence or repeated seed behavior.
Common Pitfalls
- Falling back to
ThreadLocalRandomunder load: Constructing a freshSecureRandomper request pays the provider lookup and self-seeding cost every time, so a hot token-generation path gets "optimized" withThreadLocalRandom.current()- which is ajava.util.Randomsubclass with a name that reads like a threading fix, and whose per-thread generator is as non-cryptographic as the superclass. Reuse a singlestatic final SecureRandominstead; it is thread-safe, and reusing it removes the cost that prompted the change. - Believing entropy runs out: The advice to prefer
/dev/random,NativePRNGBlockingorgetInstanceStrong()because the ordinary source "might exhaust the entropy pool" is pre-2020 folklore. Since Linux 5.6getrandom()blocks only until the CRNG is first initialised at boot,/dev/urandomnever blocks, and/dev/randomno longer blocks on an entropy estimate either. Drawing bytes does not deplete anything. A generator that blocks in production is a container or VM starting before the host kernel's CSPRNG has been seeded, which is an infrastructure problem - give the guest a virtual RNG device or an entropy daemon. Choosing a different JCA algorithm does not fix it and changing to a blocking one causes it. - Treating any
UUIDas a secure token:UUID.randomUUID()does useSecureRandominternally in modern JDKs, but only 122 of its 128 bits are random (6 are fixed for version/variant). Some codebases also swap inUUID.nameUUIDFromBytes()(version 3, MD5-based and fully deterministic from its input) as though it were a random generator.