A payments team we spoke with last quarter had a bug ticket that looked routine: password reset tokens were occasionally colliding in staging. The root cause wasn't a database race condition — it was new Random(System.currentTimeMillis()) seeding a token generator that a contractor had written two years earlier. With the seed value narrowed to a few thousand plausible millisecond timestamps, an attacker could brute-force every token the service would ever issue in under a second on a laptop. This is not a rare mistake. java.util.Random, weakly-seeded SecureRandom instances, and misuse of UUID.randomUUID() show up repeatedly in Java codebases handling session tokens, password reset links, API keys, and cryptographic nonces. This piece breaks down how secure random number generation actually works in Java, where SecureRandom and UUIDs diverge from "good enough," and what to check in your own services today.
Why is java.util.Random unsafe for security-sensitive code?
Because it's a linear congruential generator (LCG) with only a 48-bit internal seed, and that seed is fully recoverable from a small number of outputs. The algorithm, defined in the java.util.Random Javadoc since JDK 1.0, computes each next value as seed = (seed * 0x5DEECE66DL + 0xB) & ((1L << 48) - 1). Given just two or three consecutive outputs from nextInt() or nextLong(), published techniques (and public tools like java-random-cracker) can reconstruct the seed and predict every future and past value the generator will produce. Worse, if code seeds it with something guessable — System.currentTimeMillis(), a process ID, or a fixed constant left over from testing — the effective search space collapses from 2^48 to a few thousand or million candidates, well within brute-force range. java.util.Random is fine for shuffling a UI list or jittering a retry delay. It should never touch tokens, keys, nonces, or anything an attacker benefits from predicting.
What does java.security.SecureRandom actually do differently?
It sources entropy from the operating system or a certified deterministic random bit generator (DRBG) instead of a fixed arithmetic formula, making outputs computationally infeasible to predict even with full knowledge of the algorithm. SecureRandom is an abstraction over pluggable providers registered in the JVM's java.security configuration. On Linux, the default NativePRNG and SHA1PRNG providers historically pulled seed material from /dev/random or /dev/urandom; since JDK 9 (2017) and backported to JDK 8u151 (October 2017) under JEP 273, Java ships built-in DRBG implementations conforming to NIST SP 800-90A, supporting Hash_DRBG, HMAC_DRBG, and CTR_DRBG mechanisms with configurable security strengths of 112, 128, 192, or 256 bits. You select an algorithm explicitly with SecureRandom.getInstance("DRBG") or SecureRandom.getInstanceStrong(), the latter of which is required reading for anything generating cryptographic keys, since it guarantees a provider from the securerandom.strongAlgorithms list in java.security rather than whatever the platform default happens to be.
Is UUID.randomUUID() actually safe to use as a security token?
Yes, but only the version-4 form, and only if you understand what "random" means in a 128-bit UUID. java.util.UUID.randomUUID() internally calls a lazily-initialized, class-level SecureRandom instance to fill 16 bytes, then overwrites 6 fixed bits to mark the UUID as version 4 and RFC 4122 variant. That leaves 122 bits of actual entropy, not 128 — still enormous, with a collision probability low enough that generating a billion UUIDs a second for a century won't produce a meaningful collision risk. The trap is UUID.nameUUIDFromBytes(), which produces version-3 UUIDs using MD5 hashing over input bytes you supply. It is fully deterministic: the same input always yields the same UUID, with zero randomness. We've seen this method used for "unique" invite tokens and reset links, generated from predictable inputs like email address plus timestamp rounded to the nearest minute — effectively guessable. If a UUID needs to resist prediction, it must come from randomUUID(), not nameUUIDFromBytes().
What happened when a real-world SecureRandom implementation actually broke?
In August 2013, Android's implementation of Java's crypto providers shipped with a defective PRNG seeding path that left SecureRandom under-seeded on millions of devices, and the fallout hit Bitcoin wallets directly. Apps including Bitcoin Wallet, blockchain.info's Android client, and Mycelium used SecureRandom to generate the per-signature nonce (k) required by ECDSA. When two different transactions from the same address reused an identical or closely related nonce because the underlying entropy pool hadn't been properly initialized on first boot, attackers could solve two linear equations to recover the wallet's private key directly from the blockchain — no server breach, no phishing, just math applied to publicly visible signatures. Google shipped a fixed OpenSSLRandom provider and wallet vendors pushed emergency updates instructing users to move funds to new addresses. The lesson for application developers hasn't changed since: SecureRandom is only as strong as the entropy actually feeding it, and a platform-level seeding bug silently downgrades every consumer built on top of it, including code that never touches cryptography directly.
Why does SecureRandom sometimes hang or slow down startup in containers?
Because the default provider on many JVM/OS combinations blocks on /dev/random, and minimal container base images generate entropy far slower than a full OS with keyboard and disk activity to draw from. This surfaces as services that take 20-90 seconds (or hang indefinitely under load) to complete their first SecureRandom call during startup, particularly in headless JVMs on Alpine or slim Debian images without haveged or rng-tools installed. The common, and mostly safe, fix is pointing the JVM at the non-blocking device by setting -Djava.security.egd=file:/dev/./urandom or explicitly requesting SecureRandom.getInstance("NativePRNGNonBlocking"), since modern Linux kernels (5.6+, released March 2020) treat /dev/urandom as cryptographically sound once initially seeded at boot, eliminating the historical blocking-vs-security tradeoff. Teams running on older kernels or misconfigured container runtimes should verify entropy availability with cat /proc/sys/kernel/random/entropy_avail rather than blindly switching providers, since forcing a non-blocking source before the kernel pool is seeded can reintroduce the exact predictability problem SecureRandom exists to prevent.
How should teams choose between SecureRandom algorithms in practice?
By matching the algorithm to the sensitivity of the output: DRBG with getInstanceStrong() for key material and signing nonces, standard SHA1PRNG or platform-default SecureRandom for session identifiers and CSRF tokens, and NativePRNGNonBlocking where startup latency in containerized environments is a measured problem. As of JDK 17 (September 2021), Oracle's guidance explicitly recommends DRBG over the legacy SHA1PRNG, since SHA-1's role in the older algorithm is purely as a mixing function rather than a certified DRBG construction, and NIST deprecated SHA-1 for new digital signature generation in SP 800-131A. For code that needs to be portable across JVM vendors (Oracle, Temurin, Corretto, Azul), avoid hardcoding a provider name and instead rely on the securerandom.strongAlgorithms property, letting each distribution supply its validated default rather than assuming NativePRNG exists identically everywhere, since some JVM builds on Windows and macOS resolve differently than on Linux.
How Safeguard Helps
Weak randomness is a code-level decision that's invisible in a dependency manifest and easy to miss in a manual review — new Random() and SecureRandom.getInstance("SHA1PRNG") both compile cleanly and both pass unit tests that only check output length or format. Safeguard's software composition and code analysis pipeline is built to catch exactly this class of gap: static analysis rules flag java.util.Random and UUID.nameUUIDFromBytes() usage in security-relevant call paths (token generation, key derivation, session management), surface hardcoded or predictable seed values passed into SecureRandom constructors, and cross-reference the JVM and container base images in your SBOM against known entropy and PRNG advisories, including platform-level seeding defects like the 2013 Android case. Because Safeguard tracks build provenance end to end, it can also confirm which SecureRandom provider actually resolves at runtime in your deployed artifact — not just what the code requests — closing the gap between "we call SecureRandom" and "we know exactly what entropy source backs every token our service issues." For supply chain security teams, that visibility turns a subtle cryptographic footgun into a routine finding caught before it ships.