Security Resources

⌘K
  1. Home
  2. Security Resources
  3. Secure Code Explain
  4. Insecure Randomness in Security Tokens

Insecure Randomness in Security Tokens

Here is an example of Python code that is vulnerable to predictable, insecurely generated tokens:

🥺 Vulnerable Code

import random
import string
import time


def create_password_reset_token(user):
    # Vulnerable: a non-cryptographic PRNG, seeded from the clock
    random.seed(int(time.time()))
    token = "".join(random.choice(string.ascii_lowercase + string.digits) for _ in range(12))

    user.reset_token = token          # stored in clear, never expires
    user.save()
    return token

random is a Mersenne Twister built for simulations, not secrets. Seeding it with the current second means an attacker who requests a reset at the same time can regenerate the exact token offline, and observing a few outputs is enough to recover the internal state and predict future ones. The token is also stored in plaintext and has no expiry, so a database read or an old email keeps working forever.

😎 Secure Code

Here is a version of the same code that is secured against predictable, insecurely generated tokens:

import hashlib
import secrets
from datetime import datetime, timedelta, timezone


def create_password_reset_token(user):
    token = secrets.token_urlsafe(32)          # 256 bits from the OS CSPRNG

    user.reset_token_hash = hashlib.sha256(token.encode()).hexdigest()
    user.reset_token_expires = datetime.now(timezone.utc) + timedelta(minutes=15)
    user.save()

    return token                                # emailed once, never stored in clear


def consume_password_reset_token(user, candidate):
    if not user.reset_token_hash:
        return False

    digest = hashlib.sha256(candidate.encode()).hexdigest()
    if not secrets.compare_digest(digest, user.reset_token_hash):
        return False
    if datetime.now(timezone.utc) > user.reset_token_expires:
        return False

    user.reset_token_hash = None                # single use
    user.save()
    return True

secrets pulls from the operating system CSPRNG, so 256 bits of entropy make guessing hopeless. Only a hash of the token is stored, which means a leaked database row cannot be replayed, the constant-time comparison avoids a timing oracle, and the fifteen minute window plus single-use invalidation shrinks the blast radius. Use the same approach for session ids, API keys, invite codes, and CSRF tokens.