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 ofmt_rand()in PHP 7.1+, so it is equally weak.
Time-based random seeding
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_entropystill 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()andStr::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 norand(),mt_rand(), oruniqid()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