random_bytes, random_int and the new Randomizer class
Not every random number in PHP is equally secure. While rand() and mt_rand() are fine for games and simulations, they are dangerously predictable for tokens, password reset links or cryptographic keys. random_bytes, random_int and the Randomizer class from PHP 8.2 deliver genuinely cryptographically secure random values for exactly these cases.
Table of Contents
- 1. Why random quality is security relevant
- 2. random_bytes and random_int: the cryptographically secure API
- 3. Why rand() and mt_rand() are unsuitable for security code
- 4. Generating secure tokens and IDs in practice
- 5. Random numbers within a fixed value range
- 6. The Randomizer class since PHP 8.2
- 7. Keys, salts and initialization vectors
- 8. Common mistakes in custom random number generation
- 9. PHP random sources compared
- 10. Summary
- 11. FAQ
1. Why random quality is security relevant
Not every requirement for secure random numbers in PHP is the same. Anyone needing a dice simulation or a random sort order for a product list can work with an ordinary pseudo random generator without introducing security risks. The picture changes as soon as random values are used as password reset tokens, session IDs, API keys or initialization vectors for encryption. In these cases, an attacker must find it practically impossible to predict the generated value, even knowing many previous outputs of the same generator.
The difference between an ordinary and a cryptographically secure random generator lies in the internal algorithm. Ordinary generators are optimized for speed and statistical uniformity, not for unpredictability against an attacker. Generating secure random numbers in PHP therefore means deliberately choosing the right function for the given use case, instead of reflexively grabbing the first random function found in the PHP documentation. This article shows which functions are truly cryptographically secure and how to use them correctly.
2. random_bytes and random_int: the cryptographically secure API
Since version 7, PHP offers two central functions for secure random numbers: random_bytes(int $length) delivers any number of cryptographically secure random bytes as a binary string, random_int(int $min, int $max) delivers a uniformly distributed integer within a fixed range. Both functions internally use a so called CSPRNG, a Cryptographically Secure Pseudo Random Number Generator, provided at the operating system level, typically getrandom() or /dev/urandom on Linux, the CryptGenRandom API on Windows.
The decisive advantage of these secure random number functions is that they work without an external library and without manual seed management. A developer does not need to worry about where the entropy comes from, PHP fully delegates that to the operating system. Both functions throw an Exception when no sufficient entropy source is available, instead of silently falling back to a weaker source, which is important behavior for security critical code. This reliability is exactly what makes random_bytes() and random_int() the first choice for any application that needs secure random numbers.
3. Why rand() and mt_rand() are unsuitable for security code
The functions rand() and mt_rand() do not produce secure random numbers, because their underlying algorithm, the Mersenne Twister, is deterministic. If an attacker knows a sufficient number of consecutive outputs, they can reconstruct the generator's internal state and predict all future values. This behavior has long been documented in cryptography literature and has already been exploited against real applications in practice, for example to guess password reset tokens that were naively generated with mt_rand().
Another problem: mt_rand() is seeded by default with the system time or a fixed starting value if no explicit seed is passed. This initialization can, in certain environments, for example processes started in parallel at exactly the same second, lead to identical random sequences. For secure random numbers, such a mechanism must never be used, regardless of how unlikely a concrete attack may seem in an individual case. The only reliable rule of thumb is: as soon as a random value has a security relevant function, only random_bytes() or random_int() should be used.
<?php
declare(strict_types=1);
// WRONG: predictable, must never be used for security-sensitive values
$weakToken = md5((string) mt_rand());
// RIGHT: cryptographically secure random bytes
$secureToken = bin2hex(random_bytes(32));
// RIGHT: cryptographically secure random integer in a fixed range
$secureCode = random_int(100000, 999999);
echo "Weak (do not use): {$weakToken}\n";
echo "Secure token: {$secureToken}\n";
echo "Secure numeric code: {$secureCode}\n";
4. Generating secure tokens and IDs in practice
The most common practical use case for secure random numbers is generating tokens, for example for password reset links, API keys or invitation codes. The rule of thumb for length: at least 16 bytes of raw entropy (128 bits) for short lived tokens, 32 bytes (256 bits) for long lived or especially sensitive values such as API keys. Encoding should be URL safe if the token is transmitted in a URL, which is why base64url often proves more compact and practical than classic base64 or hexadecimal.
For password reset tokens, it is also important to never store the token in plaintext in the database, only its hash, usually via hash('sha256', $token). This way, the actual token remains useless to an attacker even in the event of a database leak, because they cannot compute the original value back from the stored hash. This combination of secure random numbers for generation and hashing for storage is the industry standard for token based workflows.
<?php
declare(strict_types=1);
/**
* Generates a secure, URL-safe token for password reset links.
* Only the hash is stored in the database, never the raw token.
*/
final class PasswordResetTokenFactory
{
private const TOKEN_BYTES = 32;
/**
* @return array{token: string, hash: string}
*/
public function create(): array
{
$raw = random_bytes(self::TOKEN_BYTES);
return [
'token' => rtrim(strtr(base64_encode($raw), '+/', '-_'), '='), // URL-safe
'hash' => hash('sha256', $raw),
];
}
public function matches(string $submittedToken, string $storedHash): bool
{
$raw = base64_decode(strtr($submittedToken, '-_', '+/'));
$computedHash = hash('sha256', $raw);
return hash_equals($storedHash, $computedHash);
}
}
$factory = new PasswordResetTokenFactory();
$result = $factory->create();
// Send $result['token'] to the user via email, store $result['hash'] in the database
5. Random numbers within a fixed value range
For one time passwords (OTPs), verification codes or raffle draws, one often needs secure random numbers within a concrete range, for example a six digit number between 100000 and 999999. That is exactly what random_int($min, $max) is for: the function guarantees a uniformly distributed choice within the bounds, without the well known modulo bias error that can occur with naive implementations using random_bytes() followed by the % operator.
Modulo bias occurs when the value range of the random generator is not exactly divisible by the desired range size, causing some result values to occur slightly more often than others. In cryptographic applications, for example selecting a random element from an array of key material, this seemingly small statistical flaw can actually be exploitable. random_int() internally avoids this problem through so called rejection sampling and should therefore always be preferred over a direct modulo calculation with random_bytes() whenever an integer within a range is needed.
<?php
declare(strict_types=1);
/**
* Generates a 6-digit numeric OTP using cryptographically secure randomness.
* random_int() avoids modulo bias internally via rejection sampling.
*/
function generate_otp(): string
{
return (string) random_int(100000, 999999);
}
/**
* Picks a secure random element from an array, e.g. for key rotation.
*/
function secure_array_pick(array $items): mixed
{
$index = random_int(0, count($items) - 1);
return $items[$index];
}
echo generate_otp() . "\n";
6. The Randomizer class since PHP 8.2
Since PHP 8.2, the class \Random\Randomizer provides an object oriented API that cleanly separates secure random numbers from deterministic random sequences. The constructor accepts a so called engine, which uses either \Random\Engine\Secure for cryptographically secure randomness or \Random\Engine\Mt19937 for reproducible, seeded randomness, for example in deterministic tests. This explicit separation makes it immediately visible which kind of randomness is used at a given point in the code, instead of having to guess implicitly from the function called.
The Randomizer class additionally offers convenient methods such as getBytes(), getInt(), shuffleArray() and pickArrayKeys(), which internally consistently use the chosen engine. For most security relevant cases, random_bytes() and random_int() remain the simplest choice, but as soon as a project needs both reproducible test random sequences and genuine secure random numbers, for example for different test scenarios, the Randomizer class is the cleaner architectural solution.
<?php
declare(strict_types=1);
use Random\Engine\Secure;
use Random\Engine\Mt19937;
use Random\Randomizer;
// Cryptographically secure randomizer for production use
$secureRandomizer = new Randomizer(new Secure());
$secureToken = bin2hex($secureRandomizer->getBytes(32));
// Deterministic, seeded randomizer for reproducible unit tests
$testRandomizer = new Randomizer(new Mt19937(seed: 42));
$testValue = $testRandomizer->getInt(1, 100); // always the same in tests
// Securely shuffle an array (e.g. randomizing a quiz question order)
$questions = ['q1', 'q2', 'q3', 'q4'];
$shuffled = $secureRandomizer->shuffleArray($questions);
7. Keys, salts and initialization vectors
Cryptographic operations such as symmetric encryption with AES-GCM require an initialization vector (IV) that must never be reused, as well as a key with sufficient entropy. Both values must originate from secure random numbers, otherwise a weak random source undermines the entire cryptographic construction, even if the encryption algorithm itself is considered secure. For AES-GCM, random_bytes(12) delivers a suitable 96-bit IV, for an AES-256 key, random_bytes(32) delivers the required 256 bits.
Salts for password hashing, for example in a custom HMAC construction outside of password_hash(), also need to be generated from secure random numbers. A predictable salt allows attackers to precompute rainbow tables against known salt patterns, which completely defeats the purpose of the salt, namely making every password hash computation individual. With modern password hashing functions such as password_hash() using Argon2id, PHP already handles this salt generation correctly internally, so manual salt handling is only needed for custom, non standard cryptographic constructions.
<?php
declare(strict_types=1);
/**
* Encrypts data with AES-256-GCM using a securely generated IV and key.
* Both key and IV must come from a cryptographically secure source.
*/
final class AesGcmEncryptor
{
private const CIPHER = 'aes-256-gcm';
private const IV_LENGTH = 12; // 96 bits, recommended for GCM
public function generateKey(): string
{
return random_bytes(32); // 256-bit key
}
public function encrypt(string $plaintext, string $key): array
{
$iv = random_bytes(self::IV_LENGTH); // never reuse an IV with the same key
$tag = '';
$ciphertext = openssl_encrypt(
$plaintext,
self::CIPHER,
$key,
OPENSSL_RAW_DATA,
$iv,
$tag
);
return ['ciphertext' => $ciphertext, 'iv' => $iv, 'tag' => $tag];
}
}
8. Common mistakes in custom random number generation
A recurring mistake is using uniqid() as an assumed source of secure random numbers. uniqid() is based on the current system time in microseconds and is therefore predictable within narrow bounds, especially if an attacker knows the approximate time of generation. Combining uniqid() with the more_entropy: true parameter fundamentally changes nothing about this, because the additional entropy comes from lcg_value(), another generator that is not cryptographically secure.
A second mistake is reducing token length out of convenience, for example only 8 instead of 32 bytes of raw entropy, to get shorter URLs. With too short a token length, a brute force attack becomes practically feasible, especially if there is no rate limiting on the endpoint that verifies the token. A third, often overlooked mistake concerns reusing initialization vectors in symmetric encryption: even if the IV itself comes from secure random numbers, AES-GCM's security collapses as soon as the same IV is used twice with the same key.
9. PHP random sources compared
The following table compares the most important PHP functions for random number generation and shows which use cases they are suitable or unsuitable for.
| Function | Cryptographically secure | Typical use | Assessment |
|---|---|---|---|
| rand() / mt_rand() | No | Games, simulations, UI randomness | Never for tokens or keys |
| uniqid() | No | Unique identifiers, no security relevance | Predictable via system time |
| random_bytes() | Yes | Tokens, keys, IVs | First choice for raw entropy |
| random_int() | Yes | OTPs, codes, range selection | No modulo bias |
| Randomizer (Secure) | Yes | Object oriented APIs, array shuffle | Explicit engine choice since 8.2 |
The table makes it clear: as soon as a random value fulfills a security function, only the bottom three rows are eligible. rand(), mt_rand() and uniqid() remain legitimate tools for non security relevant use cases, but must never end up in the same code path as secure random numbers, for example through copy pasting from older code.
Mironsoft
PHP security consulting and cryptography reviews
Still using mt_rand() or uniqid() in security code?
We find insecure random sources in existing PHP code, replace them with random_bytes, random_int and the Randomizer class, and thoroughly review token and encryption logic.
Code audit
Targeted search for mt_rand, rand and uniqid in security relevant code
Token design
Secure token factories for password reset, API keys and invitation codes
Cryptography review
Review of key, IV and salt generation in encryption code
10. Summary
Generating secure random numbers in PHP is not a minor detail, but a basic requirement for any application that handles tokens, keys or session values. random_bytes() and random_int() have been the right choice for nearly every security relevant case since PHP 7, because they rely on a cryptographically secure operating system generator and throw an exception in a controlled way when entropy is missing, instead of silently falling back to something weaker. The Randomizer class from PHP 8.2 onward complements these functions with an object oriented, explicitly engine based API.
rand(), mt_rand() and uniqid() remain perfectly legitimate for non security relevant use cases, but must never end up where predictability poses a risk. Anyone who consistently enforces this separation in code, for example through static analysis rules that forbid mt_rand() in security critical directories, eliminates an entire class of vulnerabilities up front.
Secure Random Numbers in PHP — The Essentials at a Glance
Raw entropy
random_bytes() for tokens, keys and initialization vectors, at least 16 to 32 bytes.
Integers in ranges
random_int($min, $max) without modulo bias for OTPs and verification codes.
Never for this
rand(), mt_rand() and uniqid() are always unsuitable for security relevant values.
Since PHP 8.2
The Randomizer class explicitly separates secure and deterministic randomness through the engine.