Skip to content

CWE-330: Use of Insufficiently Random Values - PHP

Overview

Weak random number generation in PHP occurs when developers use insecure functions like rand(), mt_rand(), or uniqid() for security-sensitive operations such as generating session tokens, password reset tokens, API keys, CSRF tokens, or cryptographic keys. These functions use predictable pseudo-random number generators (PRNGs) that are deterministic and can be exploited by attackers. For security purposes, PHP provides random_bytes() and random_int() (PHP 7.0+), which use cryptographically secure random number generators (CSPRNGs) sourced from the operating system.

Primary Defence: Use random_bytes() and random_int() (PHP 7.0+) for all security-sensitive random value generation including tokens and keys.

Common Vulnerable Patterns

rand() for Session IDs

<?php
// VULNERABLE - Predictable session ID
function generateSessionId() {
    $sessionId = rand(1000000, 9999999);
    return $sessionId;
}

// Attacker can predict: If they observe a few session IDs,
// they can infer patterns and predict future values
// rand() uses a simple LCG algorithm
?>

Why this is vulnerable: Historically, rand() varied by platform and was not cryptographically secure; since PHP 7.1 it is an alias of mt_rand(), which uses the MT19937 algorithm and is still not cryptographically secure.

mt_rand() for Password Reset Tokens

<?php
// VULNERABLE - Predictable reset token
function generateResetToken() {
    $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    $token = '';
    for ($i = 0; $i < 32; $i++) {
        $token .= $chars[mt_rand(0, strlen($chars) - 1)];
    }
    return $token;
}

// Token looks random: "a7B3xQ9..." but is predictable
// Attacker can predict based on mt_rand() seed
?>

Why this is vulnerable: mt_rand() uses the Mersenne Twister algorithm. While better than rand(), it's still not cryptographically secure and can be predicted.

uniqid() for Security Tokens

<?php
// VULNERABLE - Predictable token
function generateToken() {
    // uniqid() is based on current time in microseconds
    return uniqid('token_', true);
}

// Output: "token_6398f8a3b5c7e1.23456789"
// Attacker knowing approximate time can predict or brute force
?>

Why this is vulnerable: uniqid() is based on the current time and is highly predictable. It's not suitable for security purposes.

Time-based Seed

<?php
// VULNERABLE - Predictable seed
mt_srand(time());  // Seed with current timestamp
$token = mt_rand(0, 999999);

// Attacker knowing approximate time can brute force seed
// Time has limited entropy (~32 bits for timestamp)
?>

Why this is vulnerable: Time is predictable. Attackers can enumerate all possible timestamps within a time window and reproduce tokens.

openssl_random_pseudo_bytes() with Weak Flag

<?php
// VULNERABLE - Ignoring crypto_strong flag
$token = bin2hex(openssl_random_pseudo_bytes(16, $crypto_strong));
// Not checking $crypto_strong - might fall back to weak random

// If $crypto_strong is false, randomness is weak
?>

Why this is vulnerable: openssl_random_pseudo_bytes() may fall back to weak randomness if strong source unavailable. Ignoring the flag means you won't detect this.

array_rand() for Security

<?php
// VULNERABLE - Using array_rand for security purposes
function generateVerificationCode() {
    $digits = range(0, 9);
    $code = '';
    for ($i = 0; $i < 6; $i++) {
        $key = array_rand($digits);
        $code .= $digits[$key];
    }
    return $code;
}

// array_rand uses mt_rand internally - predictable
?>

Why this is vulnerable: array_rand() uses mt_rand() internally, making selections predictable.

str_shuffle() for Security

<?php
// VULNERABLE - Shuffling for security purposes
function generateCode($userId) {
    $chars = str_repeat('0123456789', 10);
    $shuffled = str_shuffle($chars);
    return substr($shuffled, 0, 6);
}

// str_shuffle uses rand() - highly predictable
?>

Why this is vulnerable: str_shuffle() uses rand() or mt_rand() internally. Patterns are reproducible.

Predictable Encryption Key

<?php
// VULNERABLE - Key derived from weak random
function generateEncryptionKey() {
    $key = '';
    for ($i = 0; $i < 32; $i++) {
        $key .= chr(mt_rand(0, 255));
    }
    return $key;
}

// Attacker can predict key if they know seed
?>

Why this is vulnerable: Encryption keys must have full entropy. Predictable random = predictable keys = broken encryption.

Weak Salt for Passwords

<?php
// VULNERABLE - Weak salt
function hashPassword($password) {
    $salt = mt_rand(0, 999999);  // Predictable salt
    $salted = $salt . $password;
    $hashed = hash('sha256', $salted);
    return [$hashed, $salt];
}

// Salt must be unpredictable to prevent rainbow tables
?>

Why this is vulnerable: Predictable salts can be precomputed in rainbow tables, defeating the purpose of salting.

Secure Patterns

random_bytes() for Session IDs

<?php
// SECURE - Cryptographically strong session ID
function generateSessionId() {
    // 16 bytes = 128 bits of entropy
    $randomBytes = random_bytes(16);
    $sessionId = bin2hex($randomBytes);
    return $sessionId;
}

// Example: "3a7b9c8e4f1d2a5b6c7e8f9a0b1c2d3e"
// Unpredictable even with knowledge of previous tokens
?>

Why this works:

  • random_bytes() uses OS CSPRNG: Provides cryptographically strong randomness from getrandom()//dev/urandom (Unix) or CNG/CryptGenRandom (Windows)
  • 128 bits prevents collisions: Probability 2^-128 makes collisions astronomically unlikely even with billions of IDs
  • Unpredictable output: Unlike rand()/mt_rand(), observing IDs provides no prediction capability
  • Prevents session attacks: Cryptographic randomness blocks session fixation and hijacking
  • Fail-safe behavior: Throws exception if secure randomness unavailable, never falls back to weak sources

random_bytes() with base64 for Reset Tokens

<?php
// SECURE - URL-safe reset token
function generateResetToken() {
    // 32 bytes = 256 bits of entropy
    $randomBytes = random_bytes(32);
    // URL-safe base64 encoding
    $token = rtrim(strtr(base64_encode($randomBytes), '+/', '-_'), '=');
    return $token;
}

// Example: "A3b7K9xQmZpLr4tYwFj2nVc8hG1sE6uD..."
// Can be safely used in URLs, emails
?>

Why this works:

  • 256 bits prevents brute-force: Testing 1 trillion tokens/second takes ~10^58 years to exhaust half the space
  • URL-safe base64 encoding: Replaces + with -, / with _, removes = for safe transmission in URLs/emails
  • Independent token generation: Observing millions of tokens provides no prediction capability
  • Superior to predictable schemes: Unlike timestamp/sequential tokens which can be predicted or enumerated
  • Best practices: Single-use, 1-24 hour expiration, rate limiting, store hashed in database

random_int() for Random Integers

<?php
// SECURE - Random integer in range
function generateVerificationCode() {
    // 6-digit code: 000000 to 999999
    $code = random_int(0, 999999);
    return str_pad($code, 6, '0', STR_PAD_LEFT);
}

// Example: "047382"
// Uniformly distributed, unpredictable
?>

Why this works:

  • Rejection sampling ensures uniform distribution: Unlike naive modulo which introduces bias when range doesn't divide evenly into 256
  • Each code has equal probability: Perfectly uniform distribution across 0-999999
  • ~20 bits entropy suitable for short-lived verification: Combined with rate limiting (3-5 attempts), expiration (5-15 minutes), account lockout
  • Cryptographically unpredictable: Knowledge of previous codes provides no prediction capability
  • str_pad() prevents information leakage: Always 6 digits, no magnitude hints

Password Hashing with password_hash()

<?php
// SECURE - Proper password hashing
function hashPassword($password) {
    // password_hash handles salt generation internally with CSPRNG
    // Uses bcrypt by default (PASSWORD_DEFAULT)
    $hashed = password_hash($password, PASSWORD_DEFAULT);
    return $hashed;
}

function verifyPassword($password, $hash) {
    // Constant-time comparison to prevent timing attacks
    return password_verify($password, $hash);
}

// Or explicitly use argon2id (recommended)
function hashPasswordArgon2($password) {
    $hashed = password_hash($password, PASSWORD_ARGON2ID);
    return $hashed;
}
?>

Why this works:

  • password_hash() auto-generates cryptographic salts: Uses system CSPRNG, embeds salt in output hash
  • Bcrypt (PASSWORD_DEFAULT) is computationally expensive: Cost factor (default 10) slows brute-force attacks
  • Argon2id (PASSWORD_ARGON2ID) is memory-hard: 2015 Password Hashing Competition winner, resists GPU/ASIC attacks
  • Argon2id combines best of both: Argon2i's side-channel resistance + Argon2d's time-memory trade-off resistance
  • password_verify() prevents timing attacks: Constant-time comparison blocks response-time analysis

Cryptographic Key Generation

<?php
// SECURE - Generate encryption key
function generateEncryptionKey($length = 32) {
    // 32 bytes = 256 bits for AES-256
    $key = random_bytes($length);
    return $key;
}

// For display/storage as hex
function generateEncryptionKeyHex($length = 32) {
    $key = random_bytes($length);
    return bin2hex($key);
}

// For base64 storage
function generateEncryptionKeyBase64($length = 32) {
    $key = random_bytes($length);
    return base64_encode($key);
}
?>

Why this works:

  • Highest quality randomness for encryption keys: OS CSPRNG combines hardware entropy (CPU jitter, interrupt timing, hardware RNG)
  • 256-bit AES keys provide maximum security: Resistant to brute-force and quantum attacks (128-bit post-quantum security via Grover's algorithm)
  • Raw bytes for direct use: Compatible with openssl_encrypt() and other crypto functions
  • Hex/base64 variants for storage: Suitable for databases or configuration files
  • Superior to weak alternatives: Proper KDFs (PBKDF2, Argon2, bcrypt) required if deriving from passwords; never use mt_rand()

Secure Random String Generation

<?php
// SECURE - Random password generation
function generatePassword($length = 16) {
    $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()';
    $charsLength = strlen($chars);
    $password = '';

    for ($i = 0; $i < $length; $i++) {
        // Use random_int for secure index selection
        $password .= $chars[random_int(0, $charsLength - 1)];
    }

    return $password;
}

// Example: "x7!aB9#qZ3$mK2&p"
// Each character independently random
?>

Why this works:

  • Cryptographically secure character selection: Each selection independent and uniformly distributed
  • 74 characters × 16 positions = 97 bits entropy: 74^16 ≈ 3.8 × 10^29 possibilities
  • Independent generations: Knowing previous passwords provides no prediction advantage
  • Superior to mt_rand(): Unlike MT, internal state cannot be reconstructed from outputs
  • Uniform distribution critical: Biased selection reduces entropy and aids dictionary attacks
  • Character set best practices: Include upper/lower/digits/special; optionally filter ambiguous chars (O/0/l/1/I)

CSRF Token Generation

<?php
// SECURE - CSRF token generation and validation
function generateCsrfToken() {
    // Generate 32 bytes = 256 bits
    $token = bin2hex(random_bytes(32));

    // Store in session
    $_SESSION['csrf_token'] = $token;

    return $token;
}

function validateCsrfToken($token) {
    if (!isset($_SESSION['csrf_token'])) {
        return false;
    }

    // Use hash_equals for constant-time comparison
    return hash_equals($_SESSION['csrf_token'], $token);
}
?>

Why this works:

  • 256 bits prevents guessing/brute-force: Computationally infeasible for attackers to predict valid tokens
  • Server-side session storage: Token must match submitted value for validation
  • hash_equals() prevents timing attacks: Constant-time comparison blocks byte-by-byte information leakage
  • Standard operators leak timing: ==/=== short-circuit on first mismatch, creating exploitable timing differences
  • Regenerate after auth changes: Create new tokens on login/logout
  • Validate state-changing operations: All POST/PUT/DELETE requests must include valid token

API Key Generation

<?php
// SECURE - API key with prefix and checksum
function generateApiKey() {
    // Generate 24 bytes of random data
    $randomBytes = random_bytes(24);

    // Convert to base64url encoding
    $key = rtrim(strtr(base64_encode($randomBytes), '+/', '-_'), '=');

    // Add prefix for identification
    $prefix = 'sk_live_';

    // Add checksum (optional but recommended)
    $checksum = substr(hash('sha256', $key), 0, 6);

    return $prefix . $key . '_' . $checksum;
}

// Example: "sk_live_A3b7K9xQmZpLr4tYwFj2nVc8hG1sE6uD_3f8a2e"
?>

Why this works:

  • 192 bits entropy resists brute-force: 24 bytes provides more than sufficient randomness for authentication credentials
  • base64url encoding is URL-safe: Easy to copy-paste, suitable for headers and query parameters
  • Prefix identifies key type: sk_live_ enables log recognition, prevents accidental exposure via code scanning
  • Checksum enables quick validation: Detect typos/corruption before database lookup, reduces unnecessary queries
  • Cryptographic hash prevents forgery: Checksum computed from random portion cannot be reverse-engineered
  • Best practices: Hash keys with password_hash() in database, implement rotation, rate limiting, audit logging

UUID Generation

<?php
// SECURE - Generate UUID v4
function generateUuid() {
    // Generate 16 random bytes
    $data = random_bytes(16);

    // Set version to 0100 (UUID v4)
    $data[6] = chr(ord($data[6]) & 0x0f | 0x40);

    // Set variant to 10
    $data[8] = chr(ord($data[8]) & 0x3f | 0x80);

    // Format as UUID string
    return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}

// Or use Ramsey UUID library
// composer require ramsey/uuid
use Ramsey\Uuid\Uuid;

function generateUuidLibrary() {
    return Uuid::uuid4()->toString();
}

// Example: "f47ac10b-58cc-4372-a567-0e02b2c3d479"
?>

Why this works:

  • 122 bits randomness (RFC 4122 v4): 6 bits reserved for version/variant identifiers
  • Negligible collision probability: 1 billion UUIDs/second takes ~2.7 × 10^9 years to reach 50% collision chance
  • Ideal for distributed systems: No coordination needed for unique ID generation
  • Prevents information leakage: Unlike sequential IDs, doesn't reveal record counts or creation order
  • Ramsey UUID library recommended: Well-tested, handles edge cases, supports v1/v3/v5/v6/v7
  • Binary storage optimization: 16 bytes vs 36 characters saves space and improves index performance

Key Security Functions

Token Generation Helper

<?php
/**
 * Generate secure tokens for various purposes
 * 
 * @param string $purpose Purpose identifier: 'session', 'reset', 'api', 'csrf'
 * @param int $bytes Number of random bytes (default 32 = 256 bits)
 * @return string Secure random token as hex string
 */
function generateToken($purpose, $bytes = 32) {
    $token = bin2hex(random_bytes($bytes));
    // Optionally prefix with purpose for identification
    return $purpose . '_' . $token;
}

// Usage
$sessionToken = generateToken('session');  // "session_a7b3c9..."
$resetToken = generateToken('reset', 48);  // Longer for password reset
?>

Secure Random String Generator

<?php
/**
 * Generate cryptographically secure random string
 * 
 * @param int $length Desired string length
 * @param string $charset Character set: 'alphanumeric', 'hex', 'ascii', 'digits'
 * @return string Random string
 */
function generateSecureString($length, $charset = 'alphanumeric') {
    $charsets = [
        'alphanumeric' => 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
        'hex' => '0123456789abcdef',
        'ascii' => 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()',
        'digits' => '0123456789',
    ];

    $chars = $charsets[$charset] ?? $charsets['alphanumeric'];
    $charsLength = strlen($chars);
    $result = '';

    for ($i = 0; $i < $length; $i++) {
        $result .= $chars[random_int(0, $charsLength - 1)];
    }

    return $result;
}

// Usage
$apiKey = generateSecureString(32, 'alphanumeric');
$pin = generateSecureString(6, 'digits');
?>

Entropy Checker

<?php
/**
 * Estimate entropy bits for a random value
 * 
 * @param string $value The random value
 * @param int $alphabetSize Size of character set (e.g., 62 for alphanumeric)
 * @return float Estimated entropy in bits
 */
function estimateEntropy($value, $alphabetSize) {
    $length = strlen($value);
    $entropyBits = $length * log($alphabetSize, 2);
    return $entropyBits;
}

// Usage
$token = bin2hex(random_bytes(32));  // 64 hex chars
$entropy = estimateEntropy($token, 16);  // 256 bits
echo "Token entropy: {$entropy} bits\n";  // Should be >= 128 for security

// Minimum entropy recommendations:
// - Session tokens: 128 bits
// - Password reset: 128-256 bits
// - API keys: 128-256 bits
// - Encryption keys: 128-256 bits (AES-128/AES-256)
?>

Secure Random Bytes with Fallback

<?php
/**
 * Generate cryptographically secure random bytes with error handling
 * 
 * @param int $length Number of bytes to generate
 * @return string Random bytes
 * @throws Exception If cannot generate secure random bytes
 */
function secureRandomBytes($length) {
    try {
        // PHP 7.0+ random_bytes
        return random_bytes($length);
    } catch (Exception $e) {
        // This should never happen on properly configured systems
        // Log the error and fail securely
        error_log("CRITICAL: Cannot generate secure random bytes: " . $e->getMessage());
        throw new Exception("Failed to generate secure random bytes");
    }
}

// Usage
try {
    $token = bin2hex(secureRandomBytes(32));
} catch (Exception $e) {
    // Handle error - do not fall back to weak random
    die("Security error: " . $e->getMessage());
}
?>

Verification

After implementing the recommended secure patterns, verify the fix through multiple approaches:

  • Manual testing: Generate multiple tokens/keys and verify they are unique and unpredictable with no observable patterns
  • Code review: Confirm all instances use secure functions (random_bytes(), random_int()) with no rand(), mt_rand(), or uniqid() for security purposes
  • Static analysis: Use security scanners (PHP CodeSniffer with security rules, RIPS, SonarQube) to detect weak random usage
  • Regression testing: Ensure legitimate user workflows continue to function correctly with new random generation
  • Edge case validation: Test error handling when random generation fails (though rare on properly configured systems)
  • Entropy verification: Use the entropy checker to confirm tokens meet minimum requirements (≥128 bits)
  • Authentication/session testing: Verify session tokens, CSRF tokens, and authentication mechanisms work correctly and cannot be bypassed
  • Performance testing: Ensure random generation doesn't create performance bottlenecks (though random_bytes() is generally fast)
  • Rescan: Run security scanners again to confirm the finding is resolved and no new issues were introduced

Security Checklist

  • Never use rand() or mt_rand() for security (session IDs, tokens, keys, passwords)
  • Never use uniqid() for security purposes (it's time-based)
  • Use random_bytes() (PHP 7.0+) for generating random bytes
  • Use random_int() (PHP 7.0+) for random integers in a range
  • Use password_hash() for password hashing (handles salts internally with CSPRNG)
  • Never manually seed random generators (srand(), mt_srand())
  • Ensure minimum 128 bits entropy for security-critical values
  • Use hash_equals() for constant-time token comparison
  • Check $crypto_strong flag when using openssl_random_pseudo_bytes()
  • Avoid array_rand() and str_shuffle() for security purposes
  • Use base64url encoding for URL-safe tokens: rtrim(strtr(base64_encode($bytes), '+/', '-_'), '=')
  • Run security scanners to detect weak random usage
  • Test token uniqueness in unit tests
  • Regenerate existing tokens if weak random was used
  • Implement proper error handling for random generation failures

Additional Resources