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 fromgetrandom()//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 norand(),mt_rand(), oruniqid()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()ormt_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_strongflag when usingopenssl_random_pseudo_bytes() - Avoid
array_rand()andstr_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