# Secure Random Numbers and Tokens in PHP with random_bytes

> Generate secure tokens in PHP with random_bytes and random_int: length, encoding, storing as a hash, expiry time and functions to avoid.

Source: https://phpsos.de/en/blog/sichere-tokens-php/  
As of: 25/10/2026  
Provider: PHP SOS, PHP emergency help and server security. Immediate help via WhatsApp: https://wa.me/491736494914?text=Hello%20PHP%20SOS%2C%20we%20need%20help%20with%20our%20PHP%20server.%20Company%3A%20%E2%80%A6%20Website%3A%20%E2%80%A6

**In short:** In PHP you generate secure tokens with `random_bytes()`, for example 32 bytes that you convert to text with `bin2hex()` or Base64-URL. For random numbers in a range there is `random_int()`, and for codes from a fixed set of characters, `Random\Randomizer::getBytesFromString()` since PHP 8.3. You store tokens as a hash, limit their validity, use them only once and compare them with `hash_equals()`.

## A Link That Is Too Easy to Guess

"Forgot your password?" One click, and the system sends a link with a long code. Anyone who knows the link can set the password. That is exactly why this code must be unpredictable. If it is not, an attacker needs neither the mailbox nor the old password, only enough attempts.

In reviews of PHP applications, tokens built from the time of day, the user ID or a simple random number generator are found surprisingly often. They look long and random but are not. The same applies to API keys, invitation links, sharing links for documents, confirmation codes and CSRF tokens.

The risk is real: a predictable reset link allows the takeover of any account whose email address is known, without the owner noticing anything until they can no longer log in themselves.

Since version 7.0, PHP offers a simple and secure solution. It just has to be used consistently.

## random_bytes in PHP: Where Secure Random Values Come From

According to the PHP manual, `random_bytes()` delivers cryptographically secure random bytes that are also suitable for long-lived secrets such as keys. The function uses the operating system's source of randomness, for example `getrandom()` on Linux and the CNG interface on Windows. If it finds no suitable source, it throws a `Random\RandomException` since PHP 8.2.

The overview shows which functions are suitable for secrets:

| Function | Suitable for tokens? | Note |
|---|---|---|
| `random_bytes()` | yes | secure bytes, since PHP 7.0 |
| `random_int()` | yes | secure number in a range, since PHP 7.0 |
| `Random\Randomizer` with Secure engine | yes | Secure is the default engine according to the manual |
| `rand()`, `mt_rand()` | no | predictable, only for non-security purposes |
| `uniqid()` | no | time-based, intended for uniqueness |
| `md5(time())`, `sha1($id)` | no | input guessable, the hash changes nothing about that |
| `str_shuffle()`, `array_rand()` | no | do not use a secure generator |

## Generating Tokens: Length and Encoding

32 random bytes correspond to 256 bits and are more than enough for almost any purpose. For transmission in links or headers, the token has to be converted to text. Two variants are common:

```php
<?php
declare(strict_types=1);

// Hex: 64 characters, only 0-9 and a-f
$tokenHex = bin2hex(random_bytes(32));

// Base64-URL: 43 characters, safe in URLs without further encoding
$tokenUrl = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');

// Random number in a range, for example for a selection
$index = random_int(0, 9);

// From PHP 8.3: six-digit numeric code, e.g. for confirmation by SMS
$randomizer = new \Random\Randomizer();
$code = $randomizer->getBytesFromString('0123456789', 6);
```

Short numeric codes are justifiable only if the number of attempts is strictly limited and the code expires after a few minutes. For links sent by email, always use the long token.

For API keys that partners or your own systems use permanently, two additions have proven worthwhile. A short, non-secret prefix before the random part, for example for environment or purpose, makes it easier to recognize in logs and when found in code repositories. And the key is shown only once at creation; afterwards the system knows only its hash. If a key is lost, it is replaced, not recovered.

Be careful with home-built tokens that contain information, such as user ID and expiry time in Base64. Encoding is not protection: anyone can read and change such tokens. If a token is to carry data, it must be signed with a secret key, for example with `hash_hmac('sha256', ...)`, and the signature is verified with `hash_equals()`. For more elaborate cases, reviewed libraries are preferable to your own constructions.

## Storing, Verifying and Expiring Tokens

A securely generated token is only half the battle. At least as important is how the application handles it. A proven approach for reset and invitation links:

1. **Store only the hash:** The database holds `hash('sha256', $token)`; the token itself goes to the user only via the link.
2. **Limit validity:** Store the expiry time as well and check it on redemption.
3. **Use once:** After successful use, delete the entry or mark it as used.
4. **Invalidate on changes:** After a password change, delete all open reset tokens of the account.
5. **Compare in constant time:** Where a token is compared directly with a stored value, use `hash_equals()`, with the stored value as the first parameter.

```php
<?php
// Generate
$token = bin2hex(random_bytes(32));
$eintrag = [
    'konto_id'   => $kontoId,
    'token_hash' => hash('sha256', $token),
    'gueltig_bis'=> (new DateTimeImmutable('+30 minutes'))->format('Y-m-d H:i:s'),
];
// send $token only in the link, store $eintrag

// Redeem
$gesucht = hash('sha256', (string) ($_GET['token'] ?? ''));
// look up the entry by token_hash via prepared statement,
// check expiry, then delete the entry immediately
```

Also limit the number of reset requests per account and time period so that the function cannot be abused for mass sending of emails. The response to a reset request should also not reveal whether the given address has an account at all.

A fast hash such as SHA-256 is enough here because the token itself is already long and random. This does not apply to passwords that people choose; those need slow algorithms, as [Storing Passwords Securely in PHP](https://phpsos.de/en/blog/passwoerter-in-php-speichern/) explains.

> **Tip:**
> Tokens do not belong in log files. Check whether your web server logs complete URLs with query parameters, and whether error reports send request data along.

## After an Incident: Renewing Tokens and Keys

Anyone who could execute code on a server usually also had access to stored tokens, API keys and session data. If, after PHP2Shell or another incident, there is a suspicion of a break-in, you should therefore renew not only passwords but also long-lived tokens and keys: API access, signing keys for cookies or links, access to payment and shipping services.

Sessions are a special case: PHP generates session IDs itself. How to secure their settings and end sessions after an incident is described in the guide [Securing Sessions in PHP](https://phpsos.de/en/ratgeber/session-sicherheit-php/). For form tokens against forged requests, see [CSRF Protection in PHP Applications](https://phpsos.de/en/blog/csrf-schutz-php/). Further built-in tools are presented in [Built-in Security Functions of PHP](https://phpsos.de/en/php/eingebaute-sicherheitsfunktionen/).

## How does PHP SOS help?

- We find all places in your application where tokens, codes and keys are generated and replace insecure sources.
- We check storage, expiry and single use of reset, invitation and sharing links.
- We make sure tokens do not end up in logs, error reports or analytics tools.
- After an incident, we plan the orderly replacement of tokens and keys with you.

We offer the review of your own PHP software as a [security audit and penetration test](https://phpsos.de/en/leistungen/sicherheitsaudit-penetrationstest/).


## Frequently asked questions

**Why is mt_rand unsuitable for tokens?**  
mt_rand and rand are intended for statistics and games, not for secrets. Their outputs can under some circumstances be predicted. For anything an attacker must not be able to guess, use random_bytes or random_int.

**How long should a token be?**  
For password reset links, invitations or API keys, 32 random bytes are a solid choice, 64 characters as hex text. 16 bytes, that is 128 bits, is a common lower limit.

**Is uniqid a secure token?**  
No. uniqid is based on the current time and is intended for uniqueness, not secrecy. Combinations such as md5 of the time or user ID are also predictable.

**Do I have to encrypt reset tokens in the database?**  
Store a hash of the token instead, for example with SHA-256. Anyone who reads the database cannot build valid links from it. Because the token itself is already long and random, it does not need a slow algorithm as passwords do.

**What happens if the system finds no source of randomness?**  
Since PHP 8.2, random_bytes and random_int throw a Random\RandomException. Catch this case and abort the operation instead of falling back to a weaker source.

## Sources

- [PHP manual: random_bytes](https://www.php.net/manual/de/function.random-bytes.php)
- [PHP manual: Random\Randomizer::getBytesFromString](https://www.php.net/manual/de/random-randomizer.getbytesfromstring.php)
- [PHP manual: hash_equals](https://www.php.net/manual/de/function.hash-equals.php)
- [OWASP Cheat Sheet Series](https://cheatsheetseries.owasp.org/)
