Insecure Randomness in Token Generation: CWE-338 and Session Security

Using a cryptographically weak PRNG to generate session tokens, password reset links, or CSRF tokens is a silent vulnerability that's easy to introduce and hard to detect in code review. This guide covers where CWE-338 shows up, why Math.random() and time-seeded generators are exploitable, and how to generate tokens securely in JavaScript, Python, and Java.

What CWE-338 Is and Why It Matters

CWE-338 (Use of Cryptographically Weak Pseudo-Random Number Generator) describes the use of a PRNG that is suitable for statistical randomness but not for security-sensitive operations. The distinction matters because the two requirements are different: a PRNG that produces a uniform distribution across a sample of outputs is fine for simulations, game mechanics, or shuffling a playlist. A PRNG used for token generation needs to be computationally infeasible to predict — which requires a different algorithm and a source of real entropy.

The vulnerability is frequently introduced not by developers who don’t know better, but by developers who don’t think about what category of operation they’re performing. Generating a session ID, a password reset token, a CSRF token, or an API key is a security-sensitive operation. Generating a random number for a loading animation is not. The code looks the same; the requirements are completely different.

The Exploitable Patterns

JavaScript: Math.random()

Math.random() is the most common source of CWE-338 in JavaScript. It uses a non-cryptographic PRNG (typically an xorshift or similar algorithm in V8) that produces values which are predictable given a seed. Critically, V8’s implementation is seeded from the system clock at startup, and the internal state can be partially reconstructed from observed outputs.

Vulnerable pattern:

// DO NOT USE for tokens
function generateToken(length) {
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  let token = '';
  for (let i = 0; i < length; i++) {
    token += chars.charAt(Math.floor(Math.random() * chars.length));
  }
  return token;
}

If an attacker can observe several tokens generated by the same process, they can recover internal PRNG state and predict future tokens. This is not theoretical — a 2017 paper demonstrated practical recovery of V8’s PRNG state from 8 consecutive Math.random() outputs.

Secure replacement:

const { randomBytes, randomUUID } = require('crypto');

// For URL-safe base64 tokens (e.g. 32 bytes = 256 bits of entropy)
function generateSecureToken() {
  return randomBytes(32).toString('base64url');
}

// For UUID-format tokens
function generateSecureUUID() {
  return randomUUID(); // Uses crypto.getRandomValues() internally
}

Node’s crypto.randomBytes() pulls from the OS CSPRNG (/dev/urandom on Linux, BCryptGenRandom on Windows) — entropy from hardware events, not from a mathematical formula seeded by the clock.

The UUID v1 Trap

UUID v1 is time-based: the most significant bits encode a 60-bit timestamp, and the remaining bits encode the MAC address of the generating machine. It is deterministic given the timestamp and the MAC address. Generating a password reset token as a UUID v1 is a practical security vulnerability: an attacker who knows the approximate time the token was generated can enumerate the possibility space.

UUID v4 uses random bytes and is appropriate for security tokens, but only when generated by a cryptographically secure source. Python’s uuid.uuid4() uses os.urandom() and is secure. JavaScript’s crypto.randomUUID() uses crypto.getRandomValues() and is secure. Implementations that generate UUID v4 by seeding a non-crypto PRNG with the current time are CWE-338 by another name.

Check your UUID library. Some older PHP libraries, older Java libraries, and certain npm packages that implement UUID v4 without using a CSPRNG are vulnerable. If the library doesn’t document its entropy source, don’t use it for security tokens.

Python: random Module

Python’s random module uses Mersenne Twister — fast, good statistical properties, completely unsuitable for cryptographic use. The common vulnerable pattern appears in Django projects, Flask applications, and general-purpose Python services:

import random
import string

# DO NOT USE for tokens
def generate_token(length=32):
    return ''.join(random.choices(string.ascii_letters + string.digits, k=length))

Mersenne Twister’s internal state is 624 32-bit integers. After observing 624 consecutive outputs, an attacker can fully reconstruct the internal state and predict all future and past outputs. Multiple PoC implementations of this attack exist.

Secure replacement:

import secrets

# For URL-safe tokens
token = secrets.token_urlsafe(32)  # 32 bytes = 256 bits of entropy, base64url encoded

# For hex tokens
token = secrets.token_hex(32)      # 64-character hex string

# For tokens from a custom alphabet
token = ''.join(secrets.choice(alphabet) for _ in range(length))

Python’s secrets module was added in Python 3.6 specifically to address this problem. It wraps os.urandom() and is the correct choice for all security-sensitive token generation. The old random module documentation in Python 3.6+ explicitly states it should not be used for security purposes.

Java: java.util.Random

java.util.Random uses a linear congruential generator (LCG) seeded with the current timestamp in milliseconds. An LCG’s state is fully determinable from a single output — the state is only 48 bits, and the next output is completely determined by the previous one.

// DO NOT USE for tokens
Random random = new Random();
byte[] token = new byte[32];
random.nextBytes(token);

Secure replacement:

import java.security.SecureRandom;
import java.util.Base64;

SecureRandom secureRandom = new SecureRandom();
byte[] token = new byte[32];
secureRandom.nextBytes(token);
String tokenString = Base64.getUrlEncoder().withoutPadding().encodeToString(token);

java.security.SecureRandom uses the underlying platform’s CSPRNG. On Linux, it uses /dev/urandom; on Windows, it uses CryptGenRandom. Unlike new Random(), it does not need to be seeded manually — it seeds from OS entropy automatically.

Important caveat: SecureRandom.getInstance("SHA1PRNG") with setSeed() called manually produces a deterministic PRNG seeded from the value you provide. If you seed it from System.currentTimeMillis(), you have recreated the java.util.Random vulnerability with extra steps. Never call setSeed() on a SecureRandom instance being used for token generation.

Where These Vulnerabilities Appear in Practice

Password Reset Tokens

A password reset flow generates a token, stores it (usually hashed) in the database, and sends the unhashed version to the user via email. If the token is generated with a weak PRNG:

  1. An attacker requests a password reset for a target account and notes the approximate timestamp.
  2. The attacker also requests a reset for their own account and observes their token.
  3. Using the observed token and timestamp, the attacker reconstructs PRNG state (for Mersenne Twister, this requires observing 624 32-character tokens — impractical for most deployments, but linear congruential generators need only one or two).
  4. The attacker predicts the target’s token and uses it to reset the target’s password.

Password reset tokens should be at minimum 128 bits from a CSPRNG, typically rendered as hex or base64url, with a short expiry (15-60 minutes).

Session IDs

Session IDs generated with weak PRNGs are predictable. An attacker who can observe multiple session IDs (through traffic sniffing, access logs, or observation of their own sessions over time) can predict session IDs issued before and after the observed window.

Most web frameworks handle session ID generation correctly — Express sessions, Django, Rails, Spring Session all use OS-level entropy. The vulnerability appears when developers generate their own session identifiers instead of using the framework’s session management, or when using a framework with a known weak session ID implementation (some PHP versions before 7.1 had session.entropy_length set too low).

CSRF Tokens

CSRF tokens are generated per-request or per-session and embedded in forms to verify that a form submission originated from the application’s own frontend. A predictable CSRF token defeats the protection entirely — an attacker can generate valid CSRF tokens without access to the user’s session.

API Keys and Client Secrets

API keys generated at account creation time are rarely cycled and are often long-lived. A key generated with a weak PRNG at a predictable time (e.g., when a new account is created) is potentially recoverable. This is particularly relevant for multi-tenant SaaS applications where an attacker can observe multiple API keys generated around the same time.

Minimum Security Requirements for Token Generation

A security token (session ID, CSRF token, password reset link, API key) should meet these requirements:

RequirementValue
Entropy sourceOS CSPRNG (/dev/urandom, BCryptGenRandom)
Token lengthMinimum 128 bits; 256 bits for long-lived tokens
EncodingHex or base64url (not base64 to avoid + and / in URLs)
ComparisonConstant-time comparison to prevent timing attacks

For constant-time comparison:

# Python
import hmac
hmac.compare_digest(stored_token, provided_token)
// Node.js
const { timingSafeEqual } = require('crypto');
const stored = Buffer.from(storedToken);
const provided = Buffer.from(providedToken);
timingSafeEqual(stored, provided);

String comparison with === or .equals() short-circuits on the first differing byte — a timing side-channel that can leak token prefixes under controlled network conditions.

Finding CWE-338 in Code Review

In code review, search for:

  • Math.random() in paths that generate tokens, IDs, or keys
  • new Random() (Java) anywhere outside of test code
  • random.random(), random.randint(), random.choice() (Python random module) in authentication or session flows
  • UUID generation without verification that it uses a CSPRNG-backed implementation
  • Custom token generation functions rather than framework-provided utilities

The fix is almost always a one-line change to the entropy source. The difficulty is finding the instances.