Weak Randomness and Predictable Token Generation

Weak Randomness and Predictable Token Generation - article cover image Security
Time it takes to read this article 6 minutes.

Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.

Introduction

Security tokens — session IDs, password-reset tokens, API keys, CSRF tokens, email-verification links — are only as strong as the randomness behind them. If a token can be predicted, guessed, or narrowed to a small search space, the token’s length and encoding are irrelevant: a 128-bit hex string generated from a 32-bit predictable seed offers on the order of 2^32 effective security at best, and often much less once the generator’s internal state can be inferred from observed output. Weak randomness is a quiet, systemic bug class because it fails silently — the token *looks* random, the application works correctly in every functional test, and nothing breaks until someone deliberately analyzes the generator.

The single most common instance of this bug is using a general-purpose pseudo-random number generator (PRNG) designed for statistical distribution — Math.random() in JavaScript, rand() in C, java.util.Random, Python’s default random module — for a security-sensitive value. These generators are explicitly documented as not cryptographically secure: they use fast, non-cryptographic algorithms (commonly variants of xorshift, a linear congruential generator, or the Mersenne Twister) whose internal state can often be recovered from a modest number of observed outputs, after which every past and future output becomes predictable.

Attack Prerequisites

Exploiting weak token randomness generally requires:

  • A token generated by a non-CSPRNG sourceMath.random(), java.util.Random, an LCG, a low-entropy seed (PID, timestamp, microtime()), or a CSPRNG improperly seeded/reused.
  • A way to observe one or more generated outputs, either tokens for the attacker’s own account (very common — e.g. self-registration flows) or leaked tokens from logs, referrer headers, or a public feed.
  • A bounded or predictable state space — knowledge (or a reasonable guess) of the generator’s seed source, such as server boot time, request timestamp, or a small entropy pool.
  • A verification endpoint that accepts guesses without adequate rate limiting — password reset, session lookup, or API key validation that can be brute-forced or targeted with a predicted candidate.

How It Works

Non-cryptographic PRNGs are built for speed and statistical uniformity, not unpredictability against an adversary. Math.random() in V8 (and most JavaScript engines) is implemented with xorshift128+, a generator with only 128 bits of internal state and a fully deterministic, publicly known state-transition function; academic and public research has repeatedly shown that observing a modest number of consecutive outputs is enough to recover the internal state and predict all future (and past) outputs of that generator instance. java.util.Random is even weaker — a 48-bit linear congruential generator whose state can be recovered from as few as two or three consecutive outputs using well-documented lattice/LCG-inversion techniques.

A second, distinct failure mode is poor seeding even of an otherwise acceptable generator: seeding with the current time in seconds, the process ID, or a small counter collapses the effective entropy to whatever range those values can take, so an attacker only needs to brute-force plausible seed values (often thousands, not billions) and replay the same generation algorithm to reproduce a target’s token. This was the exact root cause of several classic PHP/CGI session-ID and token vulnerabilities where session identifiers were derived from microtime(), the client IP, and small internal counters.

The fix is to use an operating-system-backed cryptographically secure PRNG (CSPRNG)/dev/urandom on Linux, CryptGenRandom/BCryptGenRandom on Windows, exposed through language APIs like Python’s secrets module, Node’s crypto.randomBytes, and Java’s java.security.SecureRandom. These are seeded from OS-level entropy sources (interrupt timing, hardware RNGs) and designed so that observing output reveals nothing computationally feasible about internal state or future output.

Vulnerable Code / Configuration

The single most-repeated version of this bug — a password-reset token built from Math.random():

// VULNERABLE: Math.random() is not a CSPRNG
function generateResetToken() {
  return Math.random().toString(36).slice(2) +
         Math.random().toString(36).slice(2);
}

// stored and emailed as the password-reset link:
// https://app.example/reset?token=k3j1s0z9f...
JavaScript

The same mistake in Python, seeded implicitly by the interpreter’s default Mersenne Twister PRNG rather than the secrets module:

import random, string

def generate_api_key():
    # VULNERABLE: random.choice draws from a non-CSPRNG (Mersenne Twister)
    alphabet = string.ascii_letters + string.digits
    return ''.join(random.choice(alphabet) for _ in range(32))
Python

And a Java session-token generator seeded with predictable, low-entropy material — a pattern still occasionally found in legacy code:

// VULNERABLE: java.util.Random is a 48-bit LCG, and this seed is guessable
Random rnd = new Random(System.currentTimeMillis());
long sessionId = rnd.nextLong();
Java

Corrected versions replace the generator, not just the encoding, with a CSPRNG sourced from the OS entropy pool:

import secrets

def generate_reset_token():
    return secrets.token_urlsafe(32)   # 256 bits from os.urandom
Python

Walkthrough / Exploitation

Against a Math.random()-based token, the attacker first collects several consecutive outputs — trivially possible if the app exposes any Math.random() value client-side, or by requesting several reset tokens for an attacker-owned account in quick succession and observing their pattern (the V8 engine buffers 128 raw doubles per reseed, so consecutive calls share exploitable state). Public research tooling (e.g. the well-known v8-randomness-predictor class of PoCs) reconstructs the xorshift128+ state from a handful of observed doubles:

1. Trigger the token generator repeatedly for an account you control,
   capturing each Math.random()-derived value (or the raw doubles if
   exposed via an API/debug endpoint).
2. Feed the observed sequence into a state-recovery tool that solves for
   the 128-bit xorshift128+ internal state consistent with the outputs.
3. Advance the recovered state forward to predict the NEXT token the
   generator will produce for another account (e.g. the victim's).
4. Trigger the reset flow for the victim's email at the predicted moment
   and submit the predicted token before the legitimate victim does.
TEXT

Against a low-entropy-seed generator (timestamp/PID-based), the practical attack is simpler brute force rather than state recovery — enumerate plausible seed values around the known/observed request time and regenerate candidate tokens locally:

import random

# Server seeded with System.currentTimeMillis() around request_time_ms.
for ts in range(request_time_ms - 2000, request_time_ms + 2000):
    rnd = random.Random(ts)          # mirror the target's seeding logic
    candidate = rnd.getrandbits(63)
    r = requests.get(f'https://target.example/session/{candidate}')
    if r.status_code == 200:
        print('valid session found:', candidate)
        break
Python

Note: UUIDv4 is safe *only* if generated by a library that sources its random bits from a CSPRNG — most modern language UUID libraries do, but some legacy/embedded UUID implementations use a weak PRNG under the hood, which silently defeats the 122 bits of nominal randomness the format implies.

Opsec: When you find a token that looks random but is short, sequential-feeling, or reproducible across restarts, generate a batch (10-50) of tokens from your own test account and run basic statistical/pattern checks (repeated substrings, obvious counters, timestamp correlation) before investing time in full generator state recovery.

Detection and Defense

Fixing this class of bug means auditing every place a security-relevant value is generated:

  • Use a CSPRNG exclusively for security tokens: secrets (Python), crypto.randomBytes/crypto.getRandomValues (Node/browser), SecureRandom (Java), RandomNumberGenerator/RNGCryptoServiceProvider (.NET), /dev/urandom or getrandom(2) (native code).
  • Never seed a CSPRNG or non-CSPRNG with predictable material (timestamps, PIDs, counters) for anything security-relevant.
  • Use at least 128 bits of entropy for session/reset/API tokens, encoded with URL-safe base64 or hex, and treat the token as a bearer credential — single-use, short-lived, and transmitted only over TLS.
  • Rate-limit and monitor token-verification endpoints (password reset, session lookup) so even a partially predictable token cannot be brute-forced in bulk.
  • Grep dependencies and code for Math.random, java.util.Random, rand(), and bare random. module usage anywhere near words like token, session, key, password, reset, or csrf as a standard SAST/code-review rule.

Real-World Impact

Predictable token generation has been a recurring root cause in real incidents and bug bounty reports: password-reset tokens generated with Math.random() have allowed account takeover once researchers demonstrated V8 state recovery from a handful of samples, and legacy PHP/session frameworks that derived session IDs from microtime() and small counters were a well-documented source of session hijacking in the 2000s and early 2010s. It remains a common finding today in bug bounty programs whenever an application rolls its own token generator instead of using the platform’s CSPRNG helper, and is one of the fastest wins for an assessor once a non-CSPRNG source is spotted in code or in observable token patterns.

Conclusion

A token’s apparent randomness is meaningless if the generator behind it is not cryptographically secure — statistical PRNGs are fast and well-distributed but explicitly not designed to resist an adversary who can observe output or guess the seed. The fix is simple and has no real performance cost: always generate security-sensitive values from the platform’s CSPRNG, never from Math.random(), java.util.Random, or a timestamp-seeded generator, and treat any deviation from that rule as a critical finding regardless of the token’s apparent length.

You Might Also Like

If you found this useful, these related deep-dives cover adjacent techniques and their defenses:

Comments

Copied title and URL