Skip to content

CWE-331: Insufficient Entropy - PHP

Overview

Insufficient entropy in PHP occurs when using rand(), mt_rand(), uniqid(), or other predictable functions instead of cryptographically secure random number generators for security-sensitive operations like generating tokens, encryption keys, IVs, or nonces. These functions are designed for general-purpose randomness, not cryptography, and produce predictable values that can be exploited by attackers.

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

Common Vulnerable Patterns

Using mt_rand() for token generation

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

Why this is vulnerable:

  • mt_rand() uses MT19937, which is fast but not cryptographically secure.
  • An attacker can recover the internal state from enough outputs and predict future values.
  • rand() is just an alias of mt_rand() in PHP 7.1+, so it is equally weak.

Time-based random seeding

<?php
// VULNERABLE - Time-based seed
mt_srand(time());
$sessionId = mt_rand(0, 1000000);

Why this is vulnerable:

  • time() has only seconds-level precision, so the seed space is tiny.
  • An attacker can brute-force likely times and reproduce the full sequence.
  • A predictable seed makes all derived values predictable.

Using rand() for encryption keys

<?php
// VULNERABLE - Using rand for encryption key
$encryptionKey = '';
for ($i = 0; $i < 32; $i++) {
    $encryptionKey .= chr(rand(0, 255));
}

Why this is vulnerable:

  • Encryption keys must be unpredictable; rand()/mt_rand() are deterministic.
  • If the PRNG state is recovered, generated keys can be reproduced.
  • Predictable keys break the security of the encryption.

Using mt_rand() for IVs

<?php
// VULNERABLE - Random IV generation
$iv = '';
for ($i = 0; $i < 16; $i++) {
    $iv .= chr(mt_rand(0, 255));
}

Why this is vulnerable:

  • IVs/nonces must be unpredictable and never repeat under the same key.
  • mt_rand() output is predictable and can repeat across runs.
  • IV reuse in AES-GCM can catastrophically break security.

Using mt_rand() for password reset tokens

<?php
// VULNERABLE - Password reset token
$resetToken = '';
for ($i = 0; $i < 6; $i++) {
    $resetToken .= mt_rand(0, 9);
}

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.
  • mt_rand() is predictable, enabling token prediction.

Using uniqid() for API keys

<?php
// VULNERABLE - API key generation
function generateApiKey() {
    return uniqid('api_', true);  // Based on timestamp!
}

Why this is vulnerable:

  • uniqid() is time-based and designed for uniqueness, not secrecy.
  • more_entropy still relies on weak randomness and timing data.
  • Predictable IDs allow attackers to guess API keys.

Secure Patterns

Using random_bytes() and random_int() (PHP 7.0+)

<?php
// SECURE - Session token generation (128+ bits)
function generateSessionToken() {
    /**
     * Generate cryptographically secure session token
     * 16 bytes = 128 bits, converted to 32 hex chars
     */
    return bin2hex(random_bytes(16));
}

// SECURE - URL-safe token (base64-encoded)
function generateUrlSafeToken() {
    /**
     * Generate URL-safe token for password resets, CSRF, etc.
     * 32 bytes = 256 bits
     */
    $bytes = random_bytes(32);
    return rtrim(strtr(base64_encode($bytes), '+/', '-_'), '=');
}

// SECURE - Cryptographic key generation
function generateEncryptionKey($keySize = 32) {
    /**
     * Generate AES-256 key (256 bits = 32 bytes)
     */
    return random_bytes($keySize);
}

// SECURE - CSRF token
function generateCsrfToken() {
    /**
     * Generate CSRF protection token
     */
    $token = bin2hex(random_bytes(32));
    $_SESSION['csrf_token'] = $token;
    return $token;
}

// SECURE - API key generation
function generateApiKey() {
    /**
     * Generate API key with 256 bits of entropy
     * 32 bytes = 256 bits
     */
    $bytes = random_bytes(32);
    return rtrim(strtr(base64_encode($bytes), '+/', '-_'), '=');
}

// SECURE - Numeric PIN with sufficient entropy
function generateSecurePin($length = 6) {
    /**
     * Generate numeric PIN (use length >= 6)
     * Each digit has 3.32 bits of entropy
     * 6 digits = ~20 bits (use 8+ for better security)
     */
    $pin = '';
    for ($i = 0; $i < $length; $i++) {
        $pin .= random_int(0, 9);
    }
    return $pin;
}

// SECURE - Alphanumeric code
function generateAlphanumericCode($length = 12) {
    /**
     * Generate secure alphanumeric code
     */
    $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    $charsLength = strlen($chars);
    $code = '';

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

    return $code;
}

Why this works:

  • random_bytes() uses OS-backed CSPRNG sources, producing unpredictable output.
  • random_int() avoids modulo bias and is safe for numeric tokens.
  • Encodings like hex/base64 preserve entropy while making tokens usable.
  • The functions fail closed if secure randomness is unavailable.

Complete encryption example with secure randomness

<?php
declare(strict_types=1);

final class SecureEncryption
{
    private const CIPHER = 'aes-256-gcm';
    private const NONCE_LEN = 12;   // 96-bit nonce recommended for GCM
    private const TAG_LEN   = 16;   // 128-bit tag (full length)
    private const SALT_LEN  = 16;   // 128-bit salt is OK; consider 32 if you prefer
    private const KEY_LEN   = 32;   // 256-bit key
    private const PBKDF2_ITERS = 600000; // OWASP PBKDF2-HMAC-SHA256 guidance

    public static function generateSalt(int $size = self::SALT_LEN): string
    {
        if ($size < 16) {
            throw new InvalidArgumentException('Salt too short');
        }
        return random_bytes($size);
    }

    public static function deriveKey(string $password, string $salt, int $keyLength = self::KEY_LEN): string
    {
        if ($keyLength !== self::KEY_LEN) {
            throw new InvalidArgumentException('Unexpected key length');
        }
        return hash_pbkdf2(
            'sha256',
            $password,
            $salt,
            self::PBKDF2_ITERS,
            $keyLength,
            true
        );
    }

    /**
     * Returns a single base64 string containing: salt || nonce || tag || ciphertext
     */
    public static function encryptToBase64(string $plaintext, string $password): string
    {
        $salt = self::generateSalt();
        $key  = self::deriveKey($password, $salt);

        $nonce = random_bytes(self::NONCE_LEN);
        $tag = '';

        $ciphertext = openssl_encrypt(
            $plaintext,
            self::CIPHER,
            $key,
            OPENSSL_RAW_DATA,
            $nonce,
            $tag,
            $aad = '',
            self::TAG_LEN
        );

        if ($ciphertext === false) {
            throw new RuntimeException('Encryption failed');
        }

        if (strlen($tag) !== self::TAG_LEN) {
            throw new RuntimeException('Unexpected tag length');
        }

        return base64_encode($salt . $nonce . $tag . $ciphertext);
    }

    public static function decryptFromBase64(string $blobB64, string $password): string
    {
        $blob = base64_decode($blobB64, true);
        if ($blob === false) {
            throw new InvalidArgumentException('Invalid base64');
        }

        $minLen = self::SALT_LEN + self::NONCE_LEN + self::TAG_LEN + 1;
        if (strlen($blob) < $minLen) {
            throw new InvalidArgumentException('Ciphertext blob too short');
        }

        $offset = 0;
        $salt  = substr($blob, $offset, self::SALT_LEN);  $offset += self::SALT_LEN;
        $nonce = substr($blob, $offset, self::NONCE_LEN); $offset += self::NONCE_LEN;
        $tag   = substr($blob, $offset, self::TAG_LEN);   $offset += self::TAG_LEN;
        $ciphertext = substr($blob, $offset);

        // Critical: ensure tag length is exactly what you expect (PHP won't check)
        if (strlen($tag) !== self::TAG_LEN) {
            throw new InvalidArgumentException('Invalid tag length');
        }
        if (strlen($nonce) !== self::NONCE_LEN) {
            throw new InvalidArgumentException('Invalid nonce length');
        }

        $key = self::deriveKey($password, $salt);

        $plaintext = openssl_decrypt(
            $ciphertext,
            self::CIPHER,
            $key,
            OPENSSL_RAW_DATA,
            $nonce,
            $tag,
            $aad = ''
        );

        if ($plaintext === false) {
            throw new RuntimeException('Decryption or authentication failed');
        }

        return $plaintext;
    }
}

Why this works:

  • random_bytes() provides a strong salt, and PBKDF2-HMAC-SHA256 slows brute-force attacks.
  • AES-256-GCM uses a securely generated 96-bit nonce to avoid reuse.
  • Tag/nonce length validation ensures authentication fails on tampering.

Framework-Specific Guidance

Laravel - Session and CSRF Tokens

<?php
// Laravel automatically uses secure randomness for sessions and CSRF
// config/session.php
return [
    'driver' => 'database',  // Uses random_bytes internally
    // ...
];

// Generate custom secure tokens in Laravel
use Illuminate\Support\Str;

// SECURE - Laravel's secure token generator
function generateVerificationToken() {
    // Str::random() uses random_bytes internally
    return Str::random(32);
}

// SECURE - Custom token with specific length
function generateAlphanumericToken() {
    return Str::random(40);  // 40 characters
}

// SECURE - UUID generation (uses random_bytes)
function generateUuid() {
    return Str::uuid();  // UUID v4
}

// SECURE - Generate secure password reset token
use Illuminate\Support\Facades\Password;

function sendPasswordResetLink($email) {
    // Laravel's Password::broker() uses random_bytes for tokens
    Password::sendResetLink(['email' => $email]);
}

Why this works:

  • Laravel uses OS-backed CSPRNG sources for sessions, CSRF, and token generation.
  • Str::random() and Str::uuid() use secure randomness under the hood.

Symfony - Session Management

<?php
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface;

// SECURE - Symfony session ID generation
// Uses random_bytes internally
$session = new Session();
$session->start();

// SECURE - Generate secure CSRF token
class SecureTokenGenerator implements TokenGeneratorInterface {
    public function generateToken() {
        return rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
    }
}

// SECURE - Custom token generation
function generateSessionId() {
    return bin2hex(random_bytes(32));
}

Why this works:

  • Symfony session and CSRF components rely on secure random sources.
  • Custom tokens use random_bytes() directly for full entropy.

Custom API - API Key Generation and Management

<?php
class ApiKeyManager {
    /**
     * Generate secure API key with prefix and checksum
     */
    public static function generateApiKey($prefix = 'sk_live') {
        // Generate 24 bytes of random data (192 bits)
        $randomBytes = random_bytes(24);

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

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

        return "{$prefix}_{$key}_{$checksum}";
    }

    /**
     * Validate API key format
     */
    public static function validateFormat($apiKey) {
        // Check format: prefix_key_checksum
        $parts = explode('_', $apiKey);
        if (count($parts) !== 3) {
            return false;
        }

        list($prefix, $key, $providedChecksum) = $parts;

        // Verify checksum
        $expectedChecksum = substr(hash('sha256', $key), 0, 6);
        return hash_equals($expectedChecksum, $providedChecksum);
    }

    /**
     * Hash API key for storage
     */
    public static function hashApiKey($apiKey) {
        // Store hashed version in database
        return password_hash($apiKey, PASSWORD_ARGON2ID);
    }

    /**
     * Verify API key against stored hash
     */
    public static function verifyApiKey($apiKey, $hash) {
        return password_verify($apiKey, $hash);
    }
}

// Usage
$apiKey = ApiKeyManager::generateApiKey();
echo "API Key: {$apiKey}\n";
// Example: "sk_live_A3b7K9xQmZpLr4tYwFj2nVc8hG1sE6uD_3f8a2e"

// Validate format before database lookup
if (ApiKeyManager::validateFormat($apiKey)) {
    $hash = ApiKeyManager::hashApiKey($apiKey);
    // Store $hash in database, give $apiKey to user only once
}

Why this works:

  • API keys are generated from CSPRNG output and are not predictable.
  • Only hashed keys are stored, limiting exposure from database leaks.

Session Token Generation with Rotation

<?php
class SecureSessionManager {
    /**
     * Generate new session token
     */
    public static function generateSessionToken() {
        return bin2hex(random_bytes(32));  // 256 bits
    }

    /**
     * Start secure session with custom token
     */
    public static function startSession() {
        // Set secure session parameters
        ini_set('session.use_strict_mode', '1');
        ini_set('session.cookie_httponly', '1');
        ini_set('session.cookie_secure', '1');  // HTTPS only
        ini_set('session.cookie_samesite', 'Strict');

        session_start();

        // Regenerate session ID periodically
        if (!isset($_SESSION['created'])) {
            $_SESSION['created'] = time();
        } else if (time() - $_SESSION['created'] > 1800) {
            // Regenerate every 30 minutes
            session_regenerate_id(true);
            $_SESSION['created'] = time();
        }
    }

    /**
     * Generate and store CSRF token
     */
    public static function generateCsrfToken() {
        $token = bin2hex(random_bytes(32));
        $_SESSION['csrf_token'] = $token;
        return $token;
    }

    /**
     * Validate CSRF token
     */
    public static function validateCsrfToken($token) {
        if (!isset($_SESSION['csrf_token'])) {
            return false;
        }

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

// Usage
SecureSessionManager::startSession();
$csrfToken = SecureSessionManager::generateCsrfToken();

// In form
echo '<input type="hidden" name="csrf_token" value="' . htmlspecialchars($csrfToken) . '">';

// On form submission
if (!SecureSessionManager::validateCsrfToken($_POST['csrf_token'] ?? '')) {
    die('CSRF token validation failed');
}

Why this works:

  • Session IDs and CSRF tokens use random_bytes() for strong entropy.
  • Strict session settings and rotation reduce session fixation risk.

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, Psalm, PHPStan with security extensions) 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: Verify tokens meet minimum entropy requirements (≥128 bits for session tokens, ≥256 bits for long-lived tokens)
  • 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

Additional Resources