CWE-331: Insufficient Entropy - Python
Overview
Insufficient entropy in Python occurs when using random.Random() instead of secrets module or os.urandom() for security-sensitive operations like generating tokens, encryption keys, IVs, or nonces. The random module is designed for statistical purposes, not cryptography, and produces predictable values.
Primary Defence: Use secrets module (Python 3.6+) for all security-sensitive random value generation including tokens, keys, and initialization vectors.
Common Vulnerable Patterns
Using random module for token generation
import random
# VULNERABLE - Predictable token generation
def generate_token():
token = ''.join(random.choice('0123456789abcdef') for _ in range(32))
return token
Why this is vulnerable:
randomuses MT19937, which is fast but not cryptographically secure.- The internal state can be recovered from enough outputs, enabling prediction.
- Deterministic output makes tokens guessable.
Time-based random seeding
import random
import time
# VULNERABLE - Time-based seed
random.seed(int(time.time()))
session_id = random.randint(0, 1000000)
Why this is vulnerable:
time.time()has limited precision, so the seed space is small.- An attacker can brute-force likely times and reproduce the sequence.
- A predictable seed makes all values predictable.
Using random for encryption keys
import random
# VULNERABLE - Using random for encryption key
encryption_key = bytes([random.randint(0, 255) for _ in range(32)])
Why this is vulnerable:
- Encryption keys must be unpredictable;
randomis deterministic. - If the PRNG state is recovered, keys can be reproduced.
- Predictable keys break encryption security.
Using random for IVs and nonces
import random
# VULNERABLE - Random IV generation
iv = bytes([random.getrandbits(8) for _ in range(16)])
Why this is vulnerable:
- IVs/nonces must be unpredictable and never repeat under the same key.
randomoutput is predictable and can repeat across runs.- IV reuse in AES-GCM can catastrophically break security.
Using random for password reset tokens
import random
# VULNERABLE - Password reset token
reset_token = ''.join(random.choices('0123456789', k=6))
Why this is vulnerable:
- Reset tokens grant account access, so they must be unguessable.
- Six digits provides ~20 bits of entropy, which is brute-forceable.
randomis predictable, enabling token prediction.
Using random for API keys
import random
# VULNERABLE - API key generation
api_key = ''.join(random.sample('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 20))
Why this is vulnerable:
- API keys are long-lived credentials and must be unguessable.
random.sample()is predictable if the PRNG state is known.- Sampling without replacement reduces the keyspace.
Secure Patterns
Using secrets module (Python 3.6+)
import secrets
import os
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
# SECURE - Session token generation (128+ bits)
def generate_session_token():
"""Generate cryptographically secure session token (32 hex chars = 128 bits)"""
return secrets.token_hex(16) # 16 bytes = 128 bits
# SECURE - URL-safe token (base64-encoded)
def generate_url_safe_token():
"""Generate URL-safe token for password resets, CSRF, etc."""
return secrets.token_urlsafe(32) # 32 bytes = 256 bits
# SECURE - Cryptographic key generation
def generate_encryption_key(key_size=32):
"""Generate AES-256 key (256 bits = 32 bytes)"""
return secrets.token_bytes(key_size)
# SECURE - IV/nonce generation
def generate_iv(size=16):
"""Generate secure IV for AES (128 bits = 16 bytes)"""
return secrets.token_bytes(size)
# SECURE - CSRF token
def generate_csrf_token():
"""Generate CSRF protection token"""
return secrets.token_urlsafe(32)
# SECURE - API key generation
def generate_api_key():
"""Generate API key with 256 bits of entropy"""
return secrets.token_urlsafe(32) # 32 bytes = 256 bits
# SECURE - Numeric PIN with sufficient entropy
def generate_secure_pin(length=6):
"""Generate numeric PIN (use length >= 6)"""
return ''.join(secrets.choice('0123456789') for _ in range(length))
Why this works:
secretsuses OS-backed CSPRNG sources viaos.urandom().- Token helpers generate sufficient entropy for security-sensitive tokens.
secrets.choice()avoids modulo bias for PIN digits.
Using os.urandom() (all Python versions)
import os
import base64
import binascii
# SECURE - Random bytes from OS
def generate_random_bytes(size=32):
"""Generate cryptographically secure random bytes"""
return os.urandom(size)
# SECURE - Hex-encoded token
def generate_hex_token(size=32):
"""Generate hex token (64 hex chars = 256 bits)"""
return binascii.hexlify(os.urandom(size)).decode('utf-8')
# SECURE - Base64-encoded token
def generate_base64_token(size=32):
"""Generate base64-encoded token"""
return base64.urlsafe_b64encode(os.urandom(size)).decode('utf-8').rstrip('=')
Why this works:
os.urandom()reads from OS CSPRNG sources directly.- 32 bytes (256 bits) of entropy is sufficient for tokens and API keys.
- Hex/base64 encodings preserve the underlying entropy.
Complete encryption example with secure randomness
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import secrets
class SecureEncryption:
"""Example of secure encryption with proper randomness"""
@staticmethod
def generate_salt(size=16):
"""Generate salt for key derivation"""
return secrets.token_bytes(size)
@staticmethod
def derive_key(password: str, salt: bytes) -> bytes:
"""Derive encryption key from password using PBKDF2"""
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32, # 256 bits
salt=salt,
iterations=600000,
)
return kdf.derive(password.encode())
@staticmethod
def encrypt(plaintext: bytes, key: bytes) -> tuple:
"""Encrypt with AES-GCM (authenticated encryption)"""
# Generate secure nonce (96 bits for GCM)
nonce = secrets.token_bytes(12)
aesgcm = AESGCM(key)
ciphertext = aesgcm.encrypt(nonce, plaintext, None)
return nonce, ciphertext
@staticmethod
def decrypt(nonce: bytes, ciphertext: bytes, key: bytes) -> bytes:
"""Decrypt AES-GCM ciphertext"""
aesgcm = AESGCM(key)
return aesgcm.decrypt(nonce, ciphertext, None)
# Usage example
password = "user_password"
salt = SecureEncryption.generate_salt()
key = SecureEncryption.derive_key(password, salt)
plaintext = b"sensitive data"
nonce, ciphertext = SecureEncryption.encrypt(plaintext, key)
decrypted = SecureEncryption.decrypt(nonce, ciphertext, key)
assert plaintext == decrypted
Why this works:
- A CSPRNG salt plus PBKDF2 slows brute-force attacks and avoids rainbow tables.
- The 96-bit nonce is generated securely to prevent reuse.
- AES-GCM provides authenticated encryption with integrity protection.
Framework-Specific Guidance
Django - Session and CSRF Tokens
# Django automatically uses secure randomness for sessions and CSRF
# settings.py
SESSION_ENGINE = 'django.contrib.sessions.backends.db' # Uses secrets internally
CSRF_USE_SESSIONS = True
# Generate custom secure tokens in Django
from django.utils.crypto import get_random_string
# SECURE - Django's secure token generator
def generate_verification_token():
"""Django helper uses secrets module internally"""
return get_random_string(length=32)
# SECURE - Custom token with allowed characters
def generate_alphanumeric_token():
return get_random_string(
length=40,
allowed_chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
)
Why this works:
- Django uses secure randomness for session and CSRF tokens by default.
get_random_string()draws from thesecretsmodule.
Flask - Session Management
from flask import Flask, session
import secrets
app = Flask(__name__)
# SECURE - Use secrets for Flask secret key
app.config['SECRET_KEY'] = secrets.token_hex(32)
# SECURE - Generate secure session tokens
def generate_session_id():
return secrets.token_urlsafe(32)
@app.route('/login', methods=['POST'])
def login():
# Flask sessions use SECRET_KEY for signing (already secure)
session['user_id'] = user.id
session['csrf_token'] = secrets.token_hex(16)
return redirect('/dashboard')
Why this works:
SECRET_KEYand CSRF tokens come from CSPRNG output.- Flask sessions are signed with the secure key.
FastAPI - API Key Generation
from fastapi import FastAPI, HTTPException, Depends, Header
from typing import Optional
import secrets
app = FastAPI()
# Store API keys securely (use database in production)
valid_api_keys = set()
def generate_api_key() -> str:
"""Generate secure API key"""
return secrets.token_urlsafe(48)
def verify_api_key(x_api_key: Optional[str] = Header(None)):
"""Verify API key"""
if x_api_key not in valid_api_keys:
raise HTTPException(status_code=401, detail="Invalid API key")
return x_api_key
@app.post("/api/keys")
def create_api_key():
"""Create new API key"""
new_key = generate_api_key()
valid_api_keys.add(new_key)
return {"api_key": new_key}
@app.get("/protected", dependencies=[Depends(verify_api_key)])
def protected_endpoint():
return {"message": "Access granted"}
Why this works:
- API keys are generated with
secrets, notrandom. - Server-side verification ensures only issued keys are accepted.
Verification
After implementing the recommended secure patterns, verify the fix through multiple approaches:
- Manual testing: Submit malicious payloads relevant to this vulnerability and confirm they're handled safely without executing unintended operations
- Code review: Confirm all instances use the secure pattern (parameterized queries, safe APIs, proper encoding) with no string concatenation or unsafe operations
- Static analysis: Use security scanners to verify no new vulnerabilities exist and the original finding is resolved
- Regression testing: Ensure legitimate user inputs and application workflows continue to function correctly
- Edge case validation: Test with special characters, boundary conditions, and unusual inputs to verify proper handling
- Framework verification: If using a framework or library, confirm the recommended APIs are used correctly according to documentation
- Authentication/session testing: Verify security controls remain effective and cannot be bypassed (if applicable to the vulnerability type)
- Rescan: Run the security scanner again to confirm the finding is resolved and no new issues were introduced