ToolGrid
đŸ”’ In-Browser Processing
Security Engineering & Cryptography • 7 min read • September 19, 2026

Cryptographic Entropy vs. Pseudo-Randomness: Why Math.random() Fails in Credential Security

MJ
Written by Muhammad Javid & The ToolGrid Engineering Team • Lahore, Pakistan
Independent Software Developer & Systems Engineer

01. The Reset Token That Hijacked Production

A mid-sized fintech platform suffered a total administrative account takeover on a quiet Tuesday morning. No SQL injection. No stolen session cookies. No phished credentials. The attacker simply requested five consecutive password reset tokens for accounts they owned, waited roughly 250 milliseconds, and triggered a reset for the primary engineering administrator.

Within ninety seconds, the attacker logged in as the admin, rotated the database secrets, and locked the on-call team out of their own dashboard.

The root cause was buried in two lines of helper code committed three years earlier:

Vulnerable Helper (Do Not Copy)
function generateResetToken() {
    // Looks random. Passes unit tests. Completely broken in production.
    return Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2);
}

The developer who wrote that snippet assumed Math.random() generated unpredictable noise. It didn't. The attacker treated those five password reset strings as consecutive outputs of a linear pseudo-random generator, fed them into an automated solver script, and derived the engine's internal state.

Once you hold the generator's state, future outputs aren't secrets anymore. They are simple arithmetic.

02. Inside V8: How Math.random() Actually Works

To understand why this happens, look at how modern JavaScript runtimes implement Math.random(). In Chromium, Node.js, Deno, and Electron, the V8 engine relies on an algorithm named Xoroshiro128+ (which replaced the legacy MWC1616 generator in Chrome 49).

Xoroshiro128+ is an engineering marvel for what it was built to do: generate fast, statistically uniform numbers for canvas simulations, physics engines, UI layout shuffling, and Monte Carlo experiments. It executes in less than two nanoseconds because it relies strictly on bitwise shifts, rotations, and XOR additions across two 64-bit unsigned integers:

Xoroshiro128+ Core State Transition (V8 Engine)
uint64_t s0 = state[0];
uint64_t s1 = state[1];
uint64_t result = s0 + s1;

s1 ^= s0;
state[0] = rotl(s0, 55) ^ s1 ^ (s1 << 14); // Shift-XOR step
state[1] = rotl(s1, 36);                    // Rotation step

// Mantissa extraction converts 52 bits into a float [0, 1)
return (result & 0xFFFFFFFFFFFFF) * (1.0 / (1ULL << 52));

Notice something glaring? There is no one-way cryptographic primitive here. No hashing. No non-linear substitution boxes. Every operation is linear over the Galois field GF(2).

The internal state is only 128 bits wide (s0 and s1). Because the mantissa reveals 52 bits of the addition s0 + s1 on every invocation, an attacker only needs between two and five consecutive floating-point values to construct a set of linear equations.

Pass those equations to an SMT solver like Microsoft's Z3 or a lightweight Python script. Within 300 milliseconds on a standard laptop, the solver spits out the exact values of state[0] and state[1].

From that millisecond onward, the attacker knows every single number your application will generate. They can also step the algorithm backward to recover tokens generated before they started probing.

03. PRNG vs. CSPRNG: The Mathematical Divide

Engineers often conflate "uniform distribution" with "unpredictability". They are completely different mathematical properties.

Standard PRNG (Math.random)
  • Goal: Ultra-fast execution and statistical uniformity over cycles.
  • State: Small, fixed register (e.g., 64-bit or 128-bit memory footprint).
  • Next-Bit Test: Fails catastrophically. Future bits are easily computed from prior output.
  • State Reversal: Trivial with SAT/SMT solvers. No forward or backward secrecy.
CSPRNG (Crypto API)
  • Goal: Absolute unpredictability and non-reversibility under adversarial scrutiny.
  • State: Backed by OS kernel entropy pools continuously mixed with physical noise.
  • Next-Bit Test: Passes. No polynomial algorithm can predict bit k+1 with probability > 0.5.
  • State Protection: Forward secrecy guarantees that compromised future states cannot reveal past keys.

A standard PRNG guarantees that if you roll a 6-sided die six billion times, each face shows up roughly one billion times. A Cryptographically Secure Pseudo-Random Number Generator (CSPRNG) guarantees that even if an adversary watches you roll the die five thousand times in a row, they have zero mathematical edge in guessing the next roll.

04. Where Real Entropy Comes From: The Kernel Pool

Computer processors are strictly deterministic state machines. Given the exact same clock cycles, registers, and instructions, a CPU produces the exact same output every single time. So where does true unpredictability come from?

It comes from physical reality outside the silicon. Operating system kernels continuously harvest non-deterministic hardware fluctuations to seed their entropy pools:

Interrupt Timing

Microsecond jitter between keyboard strokes, mouse tracking events, and network packet arrival times.

Disk & I/O Fluctuations

Rotational latency variances and bus contention on solid-state controllers.

Silicon Thermal Noise

CPU hardware ring oscillators and on-chip thermal sensors fed via RDRAND and RDSEED.

The kernel runs these physical inputs through cryptographically secure mixing primitives (such as BLAKE2s, ChaCha20, or AES-CTR):

  • Linux: The getrandom(2) system call reads from the ChaCha20 DRBG pool initialized by /dev/urandom. It never blocks after initial boot entropy is achieved.
  • Windows: The CryptoAPI calls BCryptGenRandom (or legacy CryptGenRandom), implementing NIST SP 800-90A AES-CTR DRBG.
  • macOS & iOS: The kernel serves random streams through getentropy(2), rooted in the core Darwin CSPRNG subsystem.

When you invoke window.crypto.getRandomValues() in a browser or crypto.randomBytes() in Node.js, your application asks the browser engine to make a direct syscall into this kernel entropy pool. No userspace shortcuts. No predictable math loops.

05. The Silent Killer: Modulo Bias in Custom Generators

Even engineers who know enough to discard Math.random() often introduce another subtle statistical vulnerability into their token generators: modulo bias.

Here is the classic bug found in dozens of open-source password generator packages:

Flawed Modulo Mapping (Introduces Statistical Skew)
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; // 62 chars
const randomBytes = new Uint8Array(16);
window.crypto.getRandomValues(randomBytes);

// WRONG: Using modulo against alphabet length
const token = Array.from(randomBytes).map(b => alphabet[b % alphabet.length]).join('');

Why does this break? Do the math carefully.

A single byte (Uint8) has 256 possible states (0 through 255). Your alphabet has 62 characters.

256 ÷ 62 = 4 with a remainder of 8  →  256 = (4 × 62) + 8

Because 256 is not evenly divisible by 62, the numbers 0 through 7 have five distinct byte values mapping to them:

  • Index 0 ('A'): 0, 62, 124, 186, 248 → 5 / 256 chance (1.953%)
  • Index 7 ('H'): 7, 69, 131, 193, 255 → 5 / 256 chance (1.953%)
  • Index 8 ('I'): 8, 70, 132, 194        → 4 / 256 chance (1.562%)
  • Index 61 ('9'): 61, 123, 185, 247  → 4 / 256 chance (1.562%)

Characters 'A' through 'H' are 25% more likely to appear at every position in your token than characters 'I' through '9'.

Over a 20-character credential, this statistical skew compounds. In an offline dictionary or Hashcat attack, specialized rule masks take advantage of biased frequency distributions, pruning trillions of candidate permutations and slashing crack times by orders of magnitude.

The proper solution is rejection sampling. If a generated byte falls into the uneven remainder zone (values ≥ 248), you discard it and pull another byte from the pool. That preserves mathematical uniformity across the entire character set.

06. The Battle-Hardened Implementation

Here is how to write a production-ready, zero-dependency credential generator in client-side JavaScript that uses native kernel entropy and guarantees zero modulo bias:

Unbiased CSPRNG Token Generator
function generateSecureSecret(length = 24, alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*') {
    if (!window.crypto || !window.crypto.getRandomValues) {
        throw new Error('Cryptographically secure PRNG not supported in this runtime.');
    }

    const alphabetLength = alphabet.length;
    if (alphabetLength > 256) {
        throw new Error('Alphabet length exceeds 8-bit boundary.');
    }

    // Compute maximum unbiased threshold (largest multiple of alphabetLength <= 256)
    const maxValidByte = 256 - (256 % alphabetLength);
    
    let result = '';
    const buffer = new Uint8Array(length * 2); // Over-allocate buffer to minimize syscall frequency

    while (result.length < length) {
        window.crypto.getRandomValues(buffer);
        for (let i = 0; i < buffer.length; i++) {
            const byte = buffer[i];
            // Rejection sampling: discard biased tail
            if (byte < maxValidByte) {
                result += alphabet[byte % alphabetLength];
                if (result.length === length) break;
            }
        }
    }

    return result;
}

This implementation guarantees three properties:

  1. It sources entropy exclusively through window.crypto.getRandomValues() directly linked to kernel TRNG pools.
  2. The rejection condition byte < maxValidByte mathematically eliminates modulo bias. Every character in your alphabet has an identical probability of selection.
  3. It pre-allocates an over-sized buffer to minimize system call overhead during the rejection loop.

07. Zero-Leak In-Browser Password Generation

Many engineering teams still make the mistake of using centralized cloud APIs or third-party web services to generate system passwords and staging keys.

Think through the threat model for a moment. When an external web server generates a credential for you, that string exists in plain text on remote hardware. It traverses edge proxies, sits in web server access logs, passes through centralized memory heaps, and risks capture by application performance monitoring tools like Datadog or Sentry.

Credentials should be generated where they are consumed—inside client RAM.

ToolGrid Security Utility

Generate High-Entropy Passwords Without Server Exposure

ToolGrid's Password Generator & Entropy Analyzer runs 100% inside your local browser instance. It utilizes native Web Cryptography API primitives (crypto.getRandomValues) with strict rejection sampling, computes real-time bit entropy ($E = L \times \log_2(N)$), and sends zero network requests. Your credentials never touch a remote server.

08. Security Reviewer's Codebase Checklist

Before you approve another pull request touching tokens, session IDs, verification links, or passwords, audit for these red flags:

Grep for Math.random() in auth paths: Add an ESLint rule (no-restricted-properties) that flags any usage of Math.random in security-sensitive packages or microservices.
Enforce rejection sampling: Ensure any custom random integer or character selection routines eliminate modulo bias, or rely on standard libraries like Node's crypto.randomInt() which implements rejection sampling out of the box.
Verify minimum entropy thresholds: Session tokens and reset links must carry at least 128 bits of genuine cryptographic entropy (e.g., 16 cryptographically random bytes encoded as hex or 22 base64url characters).
Keep generation client-side: For user-facing tools and onboarding flows, avoid roundtripping generated passwords through backend APIs when browser-native CSPRNG engines can generate them with zero exposure.

Frequently Asked Questions

Can someone crack Math.random() if I hash the output with SHA-256?

No, hashing does not fix low entropy. If an attacker solves for the 128-bit internal seed state of the PRNG, they know every raw float the generator will output next. They can simply run SHA-256 over those known outputs themselves. Hashing a predictable value only creates a predictably hashed value.

Is UUID v4 cryptographically secure?

It depends entirely on the underlying generator. RFC 4122 specifies that UUID v4 must contain 122 random bits. If the UUID library calls crypto.getRandomValues() or Node's crypto.randomUUID(), it is cryptographically secure. If a legacy library or broken polyfill builds UUIDs using Math.random(), it can be reverse-engineered with an SMT solver.

Does /dev/urandom ever block on modern Linux systems?

On Linux kernels 5.6 and newer, /dev/urandom and getrandom(2) never block once the system has collected 128 bits of boot entropy (which typically completes in milliseconds after system startup). Modern kernels unify random number generation under a ChaCha20 DRBG that handles high-throughput requests without starvation.

Why shouldn't I generate temporary passwords on my API server?

Server-side generation forces credentials through network boundaries, application memory, reverse proxies, and logging pipelines before reaching the user. Client-side generation using window.crypto.getRandomValues() guarantees the raw secret exists exclusively in the user's browser memory until they explicitly submit it.

MJ
Written by Muhammad Javid & The ToolGrid Engineering Team • Lahore, Pakistan

Muhammad Javid is an independent software developer and systems engineer based in Lahore, Pakistan. He designs and maintains ToolGrid with an emphasis on client-side privacy, transparent web tooling, and browser-based cryptography.

Previous: Debugging Malformed JSON