VNX-PY-005 – Weak PRNG for Security Operations
Overview
This rule flags calls to functions from Python’s random module (random.randint, random.choice, random.random, random.uniform, random.randrange) in source files that also contain security-sensitive keywords such as password, token, secret, nonce, salt, otp, or session. The random module uses the Mersenne Twister algorithm (MT19937), which is a high-quality statistical PRNG but is explicitly documented as not suitable for security purposes. An attacker who observes enough outputs from a Mersenne Twister can reconstruct its internal 624-integer state and predict all future outputs. This maps to CWE-338: Use of Cryptographically Weak Pseudo-Random Number Generator.
Severity: Medium | CWE: CWE-338 – Use of Cryptographically Weak Pseudo-Random Number Generator
Why This Matters
The Mersenne Twister’s internal state is fully determinable from 624 consecutive 32-bit outputs — roughly 2.5 KB of observed random data. In a web application context an attacker can collect this data by making requests that return random-derived values (session IDs, CSRF tokens, password reset tokens, verification codes). Once the state is known, every future and past value from the same instance is predictable, breaking authentication, CSRF protection, and password reset flows simultaneously.
For a concrete example: if a password reset token is generated with ''.join(random.choices(string.ascii_letters, k=32)), an attacker who can observe other random-derived outputs from the same process can predict the reset token for any account without ever having access to the target’s email. The attack requires no elevated privilege — only the ability to observe enough application outputs.
What Gets Flagged
The rule fires when a .py file contains both a random module call and a security-relevant keyword, indicating that the weak PRNG is likely being used to generate a security-sensitive value.
# FLAGGED: weak token generation
import random
import string
def generate_session_token():
# random.choices is Mersenne Twister — predictable
return ''.join(random.choices(string.ascii_letters + string.digits, k=32))
# FLAGGED: weak password generation
def generate_temp_password():
return ''.join(random.choice("abcdefghijklmnopqrstuvwxyz0123456789") for _ in range(16))
# FLAGGED: weak salt generation
def make_salt():
return str(random.randint(100000, 999999))
# FLAGGED: weak OTP
otp = str(random.randint(0, 999999)).zfill(6)
Remediation
- Use
secrets.token_urlsafe()for URL-safe tokens. This generates a cryptographically random token using the OS’s CSPRNG (/dev/urandomon Unix,BCryptGenRandomon Windows) and encodes it as base64url. Thenbytesargument controls entropy — 32 bytes gives 256 bits.
import secrets
# SAFE: 256-bit cryptographically random URL-safe token
session_token = secrets.token_urlsafe(32)
# SAFE: hex-encoded random token
reset_token = secrets.token_hex(32)
- Use
secrets.randbelow()for integer ranges.
import secrets
# SAFE: cryptographically secure integer in [0, 1000000)
otp = secrets.randbelow(1_000_000)
otp_str = str(otp).zfill(6)
- Use
secrets.choice()for sampling from a sequence.
import secrets
import string
alphabet = string.ascii_letters + string.digits + string.punctuation
# SAFE: cryptographically secure character selection
password = ''.join(secrets.choice(alphabet) for _ in range(20))
- Use
os.urandom()for raw bytes when you need a byte string directly.
import os
import hashlib
# SAFE: 32 bytes of OS CSPRNG output
salt = os.urandom(32)
# Then hash: hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 600_000)
- Audit
import randomglobally. Remove therandommodule import from any module that handles authentication, sessions, CSRF, or key material. Having the import present makes it easy to accidentally call the wrong function.