Skip to content

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:

  • random uses 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; random is 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.
  • random output 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.
  • random is 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:

  • secrets uses OS-backed CSPRNG sources via os.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 the secrets module.

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_KEY and 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, not random.
  • 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

Additional Resources