CWE-338: Use of Cryptographically Weak PRNG - PHP
Overview
Use of Cryptographically Weak PRNG in PHP applications occurs when developers use non-cryptographic random number generators like rand(), mt_rand(), or uniqid() for security-sensitive operations. These functions are not cryptographically secure and should never be used for generating tokens, keys, passwords, or other security-critical values. Attackers can exploit predictable random values to compromise sessions, guess tokens, or break encryption.
Primary Defence: Use random_bytes() or random_int() (PHP 7.0+) for all security-sensitive random value generation including session tokens, CSRF tokens, API keys, and password reset tokens.
Common Vulnerable Patterns
Using rand() or mt_rand() for Security
<?php
// INSECURE: Using weak PRNG for session token
session_id(md5(rand())); // Predictable session ID
// INSECURE: Using mt_rand for CSRF token
$csrf_token = md5(mt_rand()); // Predictable token
// INSECURE: Using rand for password reset token
$reset_token = bin2hex(pack('N', rand())); // Easily guessable
// INSECURE: Using mt_rand for API key generation
$api_key = '';
for ($i = 0; $i < 32; $i++) {
$api_key .= chr(mt_rand(33, 126)); // Predictable API key
}
Why this is vulnerable:
Historically, rand() varied by platform and implementation 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. Even with MD5 hashing, if the input space is small (32-bit rand() output), attackers can brute-force or rainbow-table the hash. For session IDs, this enables session hijacking; for CSRF tokens, it allows cross-site request forgery; for password reset tokens, account takeover. PHP versions before 7.0 used an even weaker linear congruential generator for rand(). The predictable nature means an attacker who can observe a few random values from your application can predict the internal state and generate valid tokens.
Using uniqid() for Security Tokens
<?php
// INSECURE: Using uniqid for session token
$session_token = uniqid('sess_', true); // Based on time, predictable
// INSECURE: Using uniqid for password reset
$reset_token = uniqid(bin2hex(random_bytes(8)), true); // Timestamp leak
// INSECURE: Using uniqid with more_entropy still predictable
$api_key = uniqid('', true); // Still based on microsecond time
Why this is vulnerable:
The uniqid() function generates identifiers based on the current time in microseconds, with an optional prefix and entropy flag. Even with more_entropy = true, the output is primarily time-based and predictable. An attacker who knows approximately when a token was generated (from server logs, HTTP headers, or application behavior) can brute-force the small time window. The microsecond precision provides only about 20 bits of entropy per second, and the entropy flag adds a weak linear congruential generator output. For password reset tokens, this allows attackers to predict valid tokens within a narrow time window. For session IDs, it enables timing-based attacks. While uniqid() is suitable for generating temporary file names or non-security identifiers, it should never be used for authentication tokens, cryptographic keys, or any security-sensitive randomness.
Secure Patterns
Using random_bytes() for Raw Binary Randomness
<?php
// SECURE - Generate cryptographically secure random bytes
$key = random_bytes(32); // 256-bit encryption key
$iv = random_bytes(16); // 128-bit initialization vector
// SECURE - Generate secure session token
$session_token = bin2hex(random_bytes(32)); // 64-character hex token
// SECURE - Generate secure password reset token
$reset_token = base64_encode(random_bytes(32)); // Base64-encoded token
// SECURE - Generate secure CSRF token
$csrf_token = bin2hex(random_bytes(24)); // 48-character hex token
// SECURE - Generate secure API key
$api_key = base64_encode(random_bytes(32));
$api_key = rtrim(strtr($api_key, '+/', '-_'), '='); // URL-safe base64
Why this works:
random_bytes()uses OS CSPRNG:getrandom()//dev/urandom(Unix) or CNG/CryptGenRandom (Windows)- Fail-safe behavior: Throws exception if secure randomness unavailable, never silently falls back to weak sources
- 256 bits prevents brute-force: 2^256 possibilities makes attacks computationally infeasible
- Hex/base64 encoding for compatibility: Suitable for URLs, databases, HTTP headers
- Prevents prediction attacks: Unlike weak PRNGs where state can be recovered from outputs
Using random_int() for Random Integers
<?php
// SECURE - Generate secure random integer in range
$otp_code = random_int(100000, 999999); // 6-digit OTP
$verification_code = random_int(1000, 9999); // 4-digit code
// SECURE - Generate random array index securely
$items = ['apple', 'banana', 'cherry', 'date'];
$random_index = random_int(0, count($items) - 1);
$random_item = $items[$random_index];
// SECURE - Generate random delay for rate limiting (milliseconds)
$delay = random_int(100, 500);
usleep($delay * 1000);
// SECURE - Shuffle array securely (custom implementation)
function secure_shuffle(array &$array): void {
$count = count($array);
for ($i = $count - 1; $i > 0; $i--) {
$j = random_int(0, $i);
$temp = $array[$i];
$array[$i] = $array[$j];
$array[$j] = $temp;
}
}
Why this works:
random_int()uses same CSPRNG asrandom_bytes(): Cryptographically secure, not predictable likemt_rand()- Unbiased uniform distribution: Unlike
mt_rand() % rangewhich introduces statistical bias - Equal probability across range: Each value has exactly the same chance, critical for security
- Explicit failure: Throws exception if range invalid or secure randomness unavailable
- Secure array shuffling: Using
random_int()instead ofshuffle()(which usesrand()) ensures cryptographic security
Framework-Specific Examples
Laravel - Secure Token Generation
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Crypt;
class AuthController extends Controller
{
// SECURE - Laravel's Str::random uses random_bytes
public function generateApiToken()
{
$api_token = Str::random(64); // 64-character secure token
// Store hashed version in database
$hashed_token = hash('sha256', $api_token);
return response()->json([
'token' => $api_token,
'expires_at' => now()->addYear()
]);
}
// SECURE - Generate password reset token
public function generatePasswordResetToken($user)
{
$token = Str::random(60);
// Store token with expiration
DB::table('password_resets')->insert([
'email' => $user->email,
'token' => Hash::make($token),
'created_at' => now()
]);
return $token;
}
// SECURE - Generate CSRF token (Laravel does this automatically)
public function getCsrfToken()
{
return csrf_token(); // Uses Str::random internally
}
// SECURE - Generate secure session ID
public function regenerateSession()
{
request()->session()->regenerate(); // Uses random_bytes
return response()->json(['status' => 'session_regenerated']);
}
}
Why this works:
- Laravel's
Str::random()usesrandom_bytes(): Cryptographically secure by default - Token hashing before storage: Prevents leakage if database compromised
- Built-in CSRF protection: Automatically generates and validates tokens using secure randomness
- Session regeneration uses secure handling: PHP's native secure session mechanisms
- Defense-in-depth: Combination of secure generation, hashing, expiration protects against token-based attacks
Symfony - Secure Random Generation
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
class SecurityController extends AbstractController
{
private CsrfTokenManagerInterface $csrfTokenManager;
public function __construct(CsrfTokenManagerInterface $csrfTokenManager)
{
$this->csrfTokenManager = $csrfTokenManager;
}
// SECURE - Generate CSRF token
public function getCsrfToken(): Response
{
$token = $this->csrfTokenManager->getToken('form_intent');
return $this->json([
'csrf_token' => $token->getValue() // Uses random_bytes
]);
}
// SECURE - Validate CSRF token
public function validateCsrfToken(string $tokenValue): bool
{
$token = new CsrfToken('form_intent', $tokenValue);
if (!$this->csrfTokenManager->isTokenValid($token)) {
throw new InvalidCsrfTokenException('Invalid CSRF token');
}
return true;
}
// SECURE - Generate API key
public function generateApiKey(): Response
{
$apiKey = bin2hex(random_bytes(32));
// Store hashed version
$hashedKey = password_hash($apiKey, PASSWORD_ARGON2ID);
return $this->json([
'api_key' => $apiKey,
'store_hash' => $hashedKey
]);
}
// SECURE - Generate verification token
public function generateVerificationToken(): string
{
return rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
}
}
Why this works:
- Symfony CSRF manager uses
random_bytes(): Cryptographically secure token generation - Built-in CSRF protection: Framework security component handles generation and validation
- Argon2id password hashing: Resists GPU-based cracking attacks
- URL-safe base64 encoding: Tokens work correctly in URLs and HTTP headers
- Dependency injection: Easy to use secure random generation throughout application
Testing and Validation
Verify Secure Random Usage
<?php
// Test random_bytes generates different values
function test_random_bytes_uniqueness() {
$values = [];
for ($i = 0; $i < 1000; $i++) {
$values[] = bin2hex(random_bytes(16));
}
$unique = array_unique($values);
assert(count($unique) === 1000, 'random_bytes should generate unique values');
}
// Test random_int distribution (basic sanity check)
function test_random_int_distribution() {
$counts = array_fill(0, 10, 0);
for ($i = 0; $i < 10000; $i++) {
$value = random_int(0, 9);
$counts[$value]++;
}
// Each digit should appear roughly 1000 times (±20%)
foreach ($counts as $count) {
assert($count >= 800 && $count <= 1200, 'Distribution should be roughly uniform');
}
}
// Test token length
function test_token_length() {
$token = bin2hex(random_bytes(32));
assert(strlen($token) === 64, 'Token should be 64 hex characters');
}
// Run tests
test_random_bytes_uniqueness();
test_random_int_distribution();
test_token_length();
echo "All tests passed!\n";
Migration Guide
Migrating from Weak PRNG to Secure PRNG
<?php
// BEFORE (PHP < 7.0 or insecure code):
$token = md5(uniqid(mt_rand(), true));
$session_id = md5(rand());
$api_key = substr(md5(microtime()), 0, 32);
// AFTER (PHP 7.0+):
$token = bin2hex(random_bytes(32));
$session_id = bin2hex(random_bytes(32));
$api_key = bin2hex(random_bytes(32));
// Regenerate all existing tokens after migration
function regenerate_all_tokens() {
// Invalidate old session tokens
DB::table('sessions')->truncate();
// Regenerate API keys (notify users)
$users = DB::table('users')->get();
foreach ($users as $user) {
$new_api_key = bin2hex(random_bytes(32));
DB::table('users')
->where('id', $user->id)
->update(['api_key' => hash('sha256', $new_api_key)]);
// Email user with new API key
send_api_key_email($user->email, $new_api_key);
}
}