Skip to content

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 as random_bytes(): Cryptographically secure, not predictable like mt_rand()
  • Unbiased uniform distribution: Unlike mt_rand() % range which 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 of shuffle() (which uses rand()) 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() uses random_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);
    }
}

Additional Resources