Skip to content

CWE-338: Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG) - Python

Overview

Use of Cryptographically Weak PRNG in Python applications occurs when developers use non-cryptographic random number generators like random module for security-sensitive operations. The random module uses the Mersenne Twister algorithm, which is predictable and unsuitable for cryptographic purposes. Attackers can exploit this weakness to predict session tokens, API keys, passwords, or other sensitive values.

Primary Defence: Use secrets module (Python 3.6+) for all security-sensitive random value generation including tokens, passwords, and security keys.

Common Vulnerable Patterns

Using random for Session Token Generation

import random
import string
from flask import Flask, session, request, jsonify

app = Flask(__name__)
app.secret_key = 'dev_secret_key'

def generate_session_token():
    """VULNERABLE: Uses predictable random for session tokens"""
    # Mersenne Twister is predictable after observing enough outputs
    chars = string.ascii_letters + string.digits
    return ''.join(random.choice(chars) for _ in range(32))

@app.route('/login', methods=['POST'])
def login():
    username = request.json.get('username')
    password = request.json.get('password')

    # Authenticate user (simplified)
    if authenticate_user(username, password):
        # VULNERABLE - Weak session token
        session_token = generate_session_token()
        session['token'] = session_token
        session['username'] = username

        return jsonify({
            'status': 'success',
            'session_token': session_token
        })

    return jsonify({'error': 'Invalid credentials'}), 401

def authenticate_user(username, password):
    # Placeholder
    return True

Why this is vulnerable:

  • random.choice() uses Mersenne Twister (predictable)
  • Attacker can predict future tokens after observing ~624 outputs
  • Session hijacking becomes possible
  • PRNG state can be recovered from outputs

Weak Password Reset Token

import random
import hashlib
from datetime import datetime, timedelta
from django.core.mail import send_mail
from django.http import JsonResponse
from django.views import View

class PasswordResetView(View):
    def post(self, request):
        email = request.POST.get('email')

        # VULNERABLE - Weak reset token generation
        reset_token = self.generate_reset_token()

        # Store token in database
        PasswordResetToken.objects.create(
            email=email,
            token=reset_token,
            expires_at=datetime.now() + timedelta(hours=1)
        )

        # Send email
        send_mail(
            'Password Reset',
            f'Reset token: {reset_token}',
            'noreply@example.com',
            [email]
        )

        return JsonResponse({'status': 'sent'})

    def generate_reset_token(self):
        """VULNERABLE: Weak token generation"""
        # Using time and random is predictable
        timestamp = str(datetime.now().timestamp())
        random_part = str(random.randint(100000, 999999))

        # Even hashing doesn't fix the weak randomness
        token = hashlib.sha256(
            (timestamp + random_part).encode()
        ).hexdigest()

        return token

Why this is vulnerable:

  • random.randint() is predictable
  • Timestamp is guessable (clock synchronization)
  • Hashing weak random doesn't improve security
  • Attacker can brute-force or predict tokens

Weak API Key Generation

import random
import string
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

class APIKeyRequest(BaseModel):
    user_id: int
    description: str

def generate_api_key():
    """VULNERABLE: Uses random for API key generation"""
    # Weak: random module is not cryptographically secure
    chars = string.ascii_uppercase + string.digits
    prefix = 'sk_'
    key = prefix + ''.join(random.choice(chars) for _ in range(32))
    return key

@app.post("/api/keys")
async def create_api_key(request: APIKeyRequest):
    # VULNERABLE - Weak API key
    api_key = generate_api_key()

    # Store in database
    await db.api_keys.insert({
        'user_id': request.user_id,
        'key': api_key,
        'description': request.description
    })

    return {
        'api_key': api_key,
        'warning': 'Store this key securely'
    }

Why this is vulnerable:

  • API keys generated with random.choice() are predictable
  • Attacker can predict keys for other users
  • Key space is effectively reduced
  • Enables unauthorized API access

Predictable PRNG Seeding

import random
import time
import os

class VulnerableTokenGenerator:
    def __init__(self):
        # VULNERABLE - Seeding with predictable values
        seed = int(time.time()) + os.getpid()
        random.seed(seed)

    def generate_token(self):
        """Generates predictable tokens due to weak seeding"""
        return random.getrandbits(128)

    def generate_password(self, length=12):
        """VULNERABLE: Weak password generation"""
        chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
        return ''.join(random.choice(chars) for _ in range(length))

# Flask route with vulnerable token generator
from flask import Flask, request, jsonify

app = Flask(__name__)
token_gen = VulnerableTokenGenerator()

@app.route('/api/register', methods=['POST'])
def register():
    username = request.json.get('username')

    # VULNERABLE - Predictable temporary password
    temp_password = token_gen.generate_password()

    # Store user with temp password
    user = create_user(username, temp_password)

    return jsonify({
        'username': username,
        'temporary_password': temp_password,
        'message': 'Please change your password'
    })

def create_user(username, password):
    # Placeholder
    pass

Why this is vulnerable:

  • Time-based seeding is predictable (synchronized clocks)
  • PID is easily guessable
  • Attacker can recreate PRNG state
  • All subsequent random values become predictable

Weak CSRF Token Generation

import random
from flask import Flask, render_template, session, request
from flask_wtf.csrf import CSRFProtect

app = Flask(__name__)
app.secret_key = 'secret_key'

def generate_csrf_token():
    """VULNERABLE: Weak CSRF token"""
    # Using random instead of secrets
    token = ''.join(
        random.choice('0123456789abcdef') for _ in range(32)
    )
    return token

@app.route('/form')
def show_form():
    # VULNERABLE - Weak CSRF token
    csrf_token = generate_csrf_token()
    session['csrf_token'] = csrf_token

    return render_template('form.html', csrf_token=csrf_token)

@app.route('/submit', methods=['POST'])
def submit_form():
    user_token = request.form.get('csrf_token')
    session_token = session.get('csrf_token')

    if user_token != session_token:
        return 'CSRF token invalid', 403

    # Process form
    return 'Success'

Why this is vulnerable:

  • CSRF tokens must be unpredictable
  • random allows token prediction
  • Enables CSRF attacks
  • Bypasses CSRF protection

Weak Salt Generation

import random
import hashlib

class VulnerablePasswordHasher:
    def hash_password(self, password):
        """VULNERABLE: Weak salt generation"""
        # WRONG: Using random for salt generation
        salt = str(random.randint(1000000, 9999999))

        # Even with salt, weak randomness compromises security
        salted = salt + password
        hashed = hashlib.sha256(salted.encode()).hexdigest()

        return f"{salt}:{hashed}"

    def verify_password(self, password, stored):
        salt, stored_hash = stored.split(':')
        salted = salt + password
        computed_hash = hashlib.sha256(salted.encode()).hexdigest()
        return computed_hash == stored_hash

# Django view with weak password hashing
from django.http import JsonResponse
from django.views import View

class RegisterView(View):
    def post(self, request):
        username = request.POST.get('username')
        password = request.POST.get('password')

        hasher = VulnerablePasswordHasher()
        # VULNERABLE - Weak salt means rainbow tables still work
        password_hash = hasher.hash_password(password)

        # Store in database
        User.objects.create(
            username=username,
            password_hash=password_hash
        )

        return JsonResponse({'status': 'registered'})

Why this is vulnerable:

  • Salt with only 7 digits (10 million possibilities)
  • Predictable salt enables pre-computation attacks
  • Defeats the purpose of salting
  • Rainbow tables remain effective

Weak Initialization Vector (IV)

import random
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import base64

class VulnerableEncryption:
    def __init__(self, key):
        self.key = key

    def encrypt(self, plaintext):
        """VULNERABLE: Weak IV generation"""
        # WRONG: Using random for IV generation
        iv = bytes([random.randint(0, 255) for _ in range(16)])

        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        padded = pad(plaintext.encode(), AES.block_size)
        ciphertext = cipher.encrypt(padded)

        # Return IV + ciphertext
        return base64.b64encode(iv + ciphertext).decode()

    def decrypt(self, encrypted):
        data = base64.b64decode(encrypted)
        iv = data[:16]
        ciphertext = data[16:]

        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        decrypted = unpad(cipher.decrypt(ciphertext), AES.block_size)

        return decrypted.decode()

# FastAPI endpoint
from fastapi import FastAPI

app = FastAPI()
encryptor = VulnerableEncryption(b'sixteen_byte_key')

@app.post("/api/encrypt")
async def encrypt_data(data: str):
    # VULNERABLE - Weak IV compromises encryption
    encrypted = encryptor.encrypt(data)
    return {"encrypted": encrypted}

Why this is vulnerable:

  • IV must be unpredictable for CBC mode
  • Weak IV allows plaintext recovery attacks
  • Predictable IVs compromise confidentiality
  • Enables chosen-plaintext attacks

Weak UUID Generation

import random
import uuid

def generate_weak_uuid():
    """VULNERABLE: Custom UUID generation with weak randomness"""
    # WRONG: Building UUID with random
    random_bytes = bytes([random.randint(0, 255) for _ in range(16)])
    return uuid.UUID(bytes=random_bytes)

def generate_user_id():
    """VULNERABLE: Predictable user IDs"""
    # Using random to generate user IDs
    return f"user_{random.randint(100000, 999999)}"

# Django model with weak IDs
from django.db import models

class User(models.Model):
    # VULNERABLE - Weak UUID as primary key
    id = models.CharField(max_length=36, primary_key=True)
    username = models.CharField(max_length=100)

    @classmethod
    def create_user(cls, username):
        # VULNERABLE - Predictable IDs enable enumeration
        user_id = str(generate_weak_uuid())
        return cls.objects.create(id=user_id, username=username)

Why this is vulnerable:

  • Predictable UUIDs enable user enumeration
  • Attackers can guess valid user IDs
  • Privacy violation
  • Enables unauthorized access

Secure Patterns

Using secrets Module for Token Generation

import secrets
import string
from flask import Flask, session, request, jsonify
from datetime import datetime, timedelta

app = Flask(__name__)
app.secret_key = secrets.token_hex(32)

def generate_session_token(length=32):
    """Secure: Uses secrets module for cryptographically secure tokens"""
    # secrets.token_urlsafe() generates URL-safe tokens
    return secrets.token_urlsafe(length)

def generate_session_id():
    """Alternative: Using token_hex for hex tokens"""
    return secrets.token_hex(32)  # 32 bytes = 256 bits

@app.route('/login', methods=['POST'])
def login():
    username = request.json.get('username')
    password = request.json.get('password')

    if authenticate_user(username, password):
        # SECURE - Cryptographically secure session token
        session_token = generate_session_token()

        # Store in database with expiration
        SessionToken.objects.create(
            token=session_token,
            username=username,
            created_at=datetime.now(),
            expires_at=datetime.now() + timedelta(hours=24)
        )

        session['token'] = session_token
        session['username'] = username

        return jsonify({
            'status': 'success',
            'session_token': session_token
        })

    return jsonify({'error': 'Invalid credentials'}), 401

@app.route('/api/verify-session', methods=['POST'])
def verify_session():
    token = request.headers.get('Authorization', '').replace('Bearer ', '')

    session_obj = SessionToken.objects.filter(
        token=token,
        expires_at__gt=datetime.now()
    ).first()

    if not session_obj:
        return jsonify({'error': 'Invalid or expired session'}), 401

    return jsonify({'status': 'valid', 'username': session_obj.username})

def authenticate_user(username, password):
    # Actual authentication logic
    pass

Why this works:

  • secrets module designed for security: Uses os.urandom() internally, not predictable Mersenne Twister like random module
  • OS cryptographic sources: Reads from /dev/urandom (Unix) or CryptGenRandom (Windows) for true randomness
  • 256 bits prevents brute-force: 2^256 possible values makes attacks computationally impossible
  • URL-safe Base64 encoding: Automatically replaces + with -, / with _, removes = padding for web contexts
  • Suitable for all security tokens: Session tokens, API keys, password reset, CSRF tokens, any unpredictable identifier

Secure Password Reset Token

import secrets
from datetime import datetime, timedelta
from django.core.mail import send_mail
from django.http import JsonResponse
from django.views import View
from django.utils import timezone

class SecurePasswordResetView(View):
    def post(self, request):
        email = request.POST.get('email')

        # SECURE - Cryptographically secure reset token
        reset_token = secrets.token_urlsafe(32)

        # Store token in database with expiration
        PasswordResetToken.objects.create(
            email=email,
            token=reset_token,
            expires_at=timezone.now() + timedelta(hours=1)
        )

        # Send email with reset link
        reset_url = f"https://example.com/reset?token={reset_token}"
        send_mail(
            'Password Reset Request',
            f'Click here to reset your password: {reset_url}',
            'noreply@example.com',
            [email],
            fail_silently=False
        )

        return JsonResponse({'status': 'sent'})

    def verify_and_reset(self, request):
        token = request.POST.get('token')
        new_password = request.POST.get('new_password')

        # Verify token and expiration
        reset_obj = PasswordResetToken.objects.filter(
            token=token,
            expires_at__gt=timezone.now(),
            used=False
        ).first()

        if not reset_obj:
            return JsonResponse({'error': 'Invalid or expired token'}, status=400)

        # Reset password
        user = User.objects.get(email=reset_obj.email)
        user.set_password(new_password)
        user.save()

        # Mark token as used
        reset_obj.used = True
        reset_obj.save()

        return JsonResponse({'status': 'password_reset'})

Why this works:

  • High entropy: secrets.token_urlsafe(32) provides 256 bits entropy - attackers can't guess valid tokens even with email, timing info, or previous tokens
  • One-time use: used=True marking prevents replay attacks where intercepted email exploited multiple times
  • Time-limited exposure: Short expiration (typically 1 hour) creates limited attack window - compromised email becomes worthless quickly
  • Database enforcement: Query filters (expires_at__gt=timezone.now(), used=False) automatically reject stale/consumed tokens during verification
  • Safer than temp passwords: Token doesn't grant access until user proves email ownership by clicking link and actively setting new password

Secure API Key Generation

import secrets
import hashlib
from fastapi import FastAPI, HTTPException, Depends
from pydantic import BaseModel
from datetime import datetime

app = FastAPI()

class APIKeyRequest(BaseModel):
    user_id: int
    description: str
    permissions: list[str]

class APIKeyService:
    @staticmethod
    def generate_api_key(prefix='sk_live_'):
        """Generate cryptographically secure API key"""
        # Generate 32 bytes of random data
        random_part = secrets.token_urlsafe(32)

        # Add prefix for key identification
        return f"{prefix}{random_part}"

    @staticmethod
    def hash_api_key(api_key: str) -> str:
        """Hash API key for secure storage"""
        # Store hash, not plaintext
        return hashlib.sha256(api_key.encode()).hexdigest()

    @staticmethod
    async def create_api_key(user_id: int, description: str, permissions: list):
        """Create and store API key"""
        # Generate secure key
        api_key = APIKeyService.generate_api_key()

        # Hash for storage
        key_hash = APIKeyService.hash_api_key(api_key)

        # Store in database
        await db.api_keys.insert({
            'user_id': user_id,
            'key_hash': key_hash,  # Store hash, not plaintext
            'description': description,
            'permissions': permissions,
            'created_at': datetime.utcnow(),
            'last_used': None
        })

        # Return plaintext key only once
        return api_key

    @staticmethod
    async def verify_api_key(api_key: str) -> dict:
        """Verify API key and return associated data"""
        key_hash = APIKeyService.hash_api_key(api_key)

        key_record = await db.api_keys.find_one({
            'key_hash': key_hash,
            'revoked': False
        })

        if not key_record:
            return None

        # Update last used timestamp
        await db.api_keys.update({'_id': key_record['_id']}, {
            '$set': {'last_used': datetime.utcnow()}
        })

        return key_record

@app.post("/api/keys")
async def create_api_key(request: APIKeyRequest):
    """Create new API key"""
    api_key = await APIKeyService.create_api_key(
        request.user_id,
        request.description,
        request.permissions
    )

    return {
        'api_key': api_key,
        'warning': 'Store this key securely. It will not be shown again.'
    }

@app.get("/api/protected")
async def protected_endpoint(api_key: str = Depends(get_api_key_from_header)):
    """Protected endpoint requiring API key"""
    key_record = await APIKeyService.verify_api_key(api_key)

    if not key_record:
        raise HTTPException(status_code=401, detail="Invalid API key")

    return {"message": "Access granted", "user_id": key_record['user_id']}

def get_api_key_from_header(authorization: str = Header(None)):
    if not authorization or not authorization.startswith('Bearer '):
        raise HTTPException(status_code=401, detail="Missing API key")
    return authorization.replace('Bearer ', '')

Why this works:

  • High entropy: secrets.token_urlsafe(48) provides 384 bits of entropy - globally unique and cryptographically unpredictable even with billions of keys
  • Hash storage: Stores SHA-256 hashes (not plaintext) via hashlib.sha256() - if database compromised, attackers get irreversible hashes
  • One-way verification: hash_api_key() allows authentication by hashing provided key and comparing to stored hash without storing original
  • One-time display: Returns plaintext key only during creation; enforces "copy now or lose forever" - no "show me my key again" functionality
  • Conventional naming: sk_live_ prefix (Stripe-style) makes keys identifiable in logs and enables secret scanning tools to detect accidental commits

Secure CSRF Token with Django

from django.middleware.csrf import get_token
from django.views.decorators.csrf import csrf_protect
from django.http import JsonResponse
from django.shortcuts import render

@csrf_protect
def show_form(request):
    """Django automatically generates secure CSRF tokens"""
    # Django uses secrets module internally for CSRF tokens
    csrf_token = get_token(request)

    return render(request, 'form.html', {
        'csrf_token': csrf_token
    })

@csrf_protect
def submit_form(request):
    """CSRF protection is automatic with decorator"""
    if request.method == 'POST':
        # Django validates CSRF token automatically
        # Process form data
        return JsonResponse({'status': 'success'})

    return JsonResponse({'error': 'Method not allowed'}, status=405)

# For API endpoints with custom CSRF handling
import secrets

class CustomCSRFProtection:
    @staticmethod
    def generate_csrf_token():
        """Generate secure CSRF token"""
        return secrets.token_urlsafe(32)

    @staticmethod
    def verify_csrf_token(token: str, stored_token: str) -> bool:
        """Timing-safe CSRF token verification"""
        if not token or not stored_token:
            return False

        # Use secrets.compare_digest for timing-safe comparison
        return secrets.compare_digest(token, stored_token)

Why this works:

  • Framework integration: Django uses secrets module internally for cryptographically strong CSRF tokens preventing Cross-Site Request Forgery
  • Automatic injection: {% csrf_token %} template tag inserts hidden field with unique, unpredictable token for all state-changing requests (POST/PUT/DELETE)
  • Timing-safe validation: Middleware validates with constant_time_compare() preventing timing attacks where attackers infer token characters via response time measurements
  • Session binding: Token bound to user's session (session storage or signed cookie); only readable by same-origin JavaScript prevents cross-site theft
  • Built-in protection: Enabled by default in middleware stack; @csrf_protect enforces on views, @csrf_exempt allows opt-out; auto-rotates after login preventing session fixation

Secure Salt and IV Generation

import secrets
import hashlib
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import base64

class SecurePasswordHasher:
    @staticmethod
    def hash_password(password: str) -> str:
        """Hash password with secure salt"""
        # Generate cryptographically secure salt
        salt = secrets.token_bytes(32)  # 256 bits

        # Use PBKDF2 with secure salt
        from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2
        from cryptography.hazmat.primitives import hashes

        kdf = PBKDF2(
            algorithm=hashes.SHA256(),
            length=32,
            salt=salt,
            iterations=600_000
        )

        key = kdf.derive(password.encode())

        # Encode salt and hash for storage
        salt_b64 = base64.b64encode(salt).decode()
        key_b64 = base64.b64encode(key).decode()

        return f"pbkdf2_sha256$600000${salt_b64}${key_b64}"

    @staticmethod
    def verify_password(password: str, stored: str) -> bool:
        """Verify password against stored hash"""
        parts = stored.split('$')
        iterations = int(parts[1])
        salt = base64.b64decode(parts[2])
        stored_key = base64.b64decode(parts[3])

        from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2
        from cryptography.hazmat.primitives import hashes

        kdf = PBKDF2(
            algorithm=hashes.SHA256(),
            length=32,
            salt=salt,
            iterations=iterations
        )

        computed_key = kdf.derive(password.encode())

        # Timing-safe comparison
        return secrets.compare_digest(computed_key, stored_key)

class SecureEncryption:
    def __init__(self, key: bytes):
        self.key = key

    @staticmethod
    def generate_key():
        """Generate secure encryption key"""
        return secrets.token_bytes(32)  # 256 bits for AES-256

    def encrypt(self, plaintext: str) -> str:
        """Encrypt with secure IV"""
        # Generate cryptographically secure nonce/IV
        nonce = secrets.token_bytes(12)  # 96 bits for GCM

        aesgcm = AESGCM(self.key)
        ciphertext = aesgcm.encrypt(nonce, plaintext.encode(), None)

        # Combine nonce and ciphertext
        combined = base64.b64encode(nonce + ciphertext).decode()
        return combined

    def decrypt(self, encrypted: str) -> str:
        """Decrypt data"""
        combined = base64.b64decode(encrypted)
        nonce = combined[:12]
        ciphertext = combined[12:]

        aesgcm = AESGCM(self.key)
        plaintext = aesgcm.decrypt(nonce, ciphertext, None)

        return plaintext.decode()

# Flask application using secure encryption
from flask import Flask, request, jsonify

app = Flask(__name__)

# Generate and store master key securely (environment variable)
MASTER_KEY = base64.b64decode(os.environ.get('MASTER_ENCRYPTION_KEY'))
encryptor = SecureEncryption(MASTER_KEY)

@app.route('/api/encrypt', methods=['POST'])
def encrypt_data():
    data = request.json.get('data')

    # SECURE - Uses secrets for IV generation
    encrypted = encryptor.encrypt(data)

    return jsonify({'encrypted': encrypted})

Why this works:

  • Unique salts: secrets.token_bytes(32) ensures 256-bit salt uniqueness per password - even identical passwords get different hashes, preventing rainbow tables and forcing per-hash cracking
  • Salt properties: Doesn't need secrecy (stored with hash) but requires uniqueness and randomness
  • Nonce uniqueness critical: AES-GCM's os.urandom(12) generates 96-bit nonce that must never repeat with same key - reuse catastrophically breaks confidentiality via XOR analysis
  • Timing-safe verification: secrets.compare_digest() takes constant time regardless of match, preventing timing attacks during login
  • Authenticated encryption: AES-GCM generates 128-bit auth tag detecting tampering; pattern demonstrates KDF salting (Argon2/PBKDF2), unique nonces, timing-safe comparisons

Secure UUID Generation

import uuid
import secrets

def generate_secure_uuid():
    """Generate UUID4 (uses os.urandom internally)"""
    return uuid.uuid4()

def generate_secure_id():
    """Alternative: Custom secure ID generation"""
    return secrets.token_urlsafe(16)

# Django model with secure UUIDs
from django.db import models
import uuid

class User(models.Model):
    # SECURE - UUID4 uses cryptographically secure random
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    username = models.CharField(max_length=100, unique=True)
    email = models.EmailField(unique=True)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.username

class APIKey(models.Model):
    # SECURE - Custom secure ID
    key_id = models.CharField(max_length=32, primary_key=True)
    user = models.ForeignKey(User, on_delete=models.CASCADE)

    @classmethod
    def create_api_key(cls, user):
        key_id = secrets.token_urlsafe(16)
        return cls.objects.create(key_id=key_id, user=user)

Why this works:

  • Strong entropy: Uses os.urandom() internally for 122 bits of randomness (6 bits reserved for version/variant) - 2^122 possible values prevents collisions and brute-force
  • Prevents enumeration: Unpredictability stops attackers from incrementing sequential IDs to discover resources (unlike auto-incrementing integers)
  • Ideal use cases: Public REST API identifiers (/api/users/{uuid}), database primary keys exposed externally, job/task IDs, file upload identifiers
  • Universal compatibility: Standardized format (8-4-4-4-12 hex) recognized by databases (PostgreSQL native type), APIs, programming languages
  • Security trade-off: For session tokens/API secrets, prefer 256-bit secrets.token_urlsafe(); UUIDs balance security, uniqueness, and interoperability

Secure Random for Security Operations

import secrets
import os

class SecureRandomService:
    """Service for all cryptographically secure random operations"""

    @staticmethod
    def generate_token(nbytes=32):
        """Generate random token (URL-safe base64)"""
        return secrets.token_urlsafe(nbytes)

    @staticmethod
    def generate_hex_token(nbytes=32):
        """Generate hex token"""
        return secrets.token_hex(nbytes)

    @staticmethod
    def generate_bytes(nbytes=32):
        """Generate random bytes"""
        return secrets.token_bytes(nbytes)

    @staticmethod
    def generate_password(length=16, use_symbols=True):
        """Generate secure random password"""
        import string

        alphabet = string.ascii_letters + string.digits
        if use_symbols:
            alphabet += string.punctuation

        # Use secrets.choice() instead of random.choice()
        password = ''.join(secrets.choice(alphabet) for _ in range(length))

        # Ensure password meets complexity requirements
        has_upper = any(c.isupper() for c in password)
        has_lower = any(c.islower() for c in password)
        has_digit = any(c.isdigit() for c in password)

        if not (has_upper and has_lower and has_digit):
            # Regenerate if doesn't meet requirements
            return SecureRandomService.generate_password(length, use_symbols)

        return password

    @staticmethod
    def generate_otp(length=6):
        """Generate numeric OTP"""
        # Use secrets.randbelow() for random integers
        otp = ''.join(str(secrets.randbelow(10)) for _ in range(length))
        return otp

    @staticmethod
    def select_random_item(items):
        """Securely select random item from list"""
        return secrets.choice(items)

# FastAPI application using secure random
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()
random_service = SecureRandomService()

class PasswordResetRequest(BaseModel):
    email: str

@app.post("/api/reset-password")
async def request_password_reset(request: PasswordResetRequest):
    # SECURE - Generate OTP with secrets
    otp = random_service.generate_otp(6)

    # Store OTP with expiration
    await db.otps.insert({
        'email': request.email,
        'otp': otp,
        'created_at': datetime.utcnow(),
        'expires_at': datetime.utcnow() + timedelta(minutes=10)
    })

    # Send OTP via email
    send_otp_email(request.email, otp)

    return {"message": "OTP sent to email"}

@app.post("/api/generate-temp-password")
async def generate_temp_password():
    # SECURE - Generate temporary password with secrets
    temp_password = random_service.generate_password(16, use_symbols=True)

    return {
        "temporary_password": temp_password,
        "expires_in": "24 hours"
    }

Why this works:

  • Use-case optimized: token_bytes(n) for raw bytes (keys, salts, IVs), token_hex(n) for human-readable hex (2n chars), token_urlsafe(n) for URL-safe Base64 (URLs, headers, cookies), randbelow(n) for integers [0, n)
  • Consistent security: All methods use os.urandom() internally, ensuring cryptographic strength regardless of convenience function
  • Drop-in replacement: SystemRandom class uses os.urandom(), allowing code using random.choice() or random.shuffle() to upgrade by changing random instance
  • Comprehensive API: Makes cryptographically secure randomness easy to use appropriately across different scenarios

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