random_bytes, random_int und die neue Randomizer-Klasse
Nicht jede Zufallszahl in PHP ist gleich sicher. Während rand() und mt_rand() für Spiele und Simulationen ausreichen, sind sie für Tokens, Passwort-Reset-Links oder kryptographische Schlüssel gefährlich vorhersagbar. random_bytes, random_int und die Randomizer-Klasse aus PHP 8.2 liefern echte, kryptographisch sichere Zufallswerte für genau diese Fälle.
Inhaltsverzeichnis
- 1. Warum Zufallsqualität sicherheitsrelevant ist
- 2. random_bytes und random_int: die kryptographisch sichere API
- 3. Warum rand() und mt_rand() für Security-Code ungeeignet sind
- 4. Sichere Tokens und IDs in der Praxis erzeugen
- 5. Zufallszahlen in einem festen Wertebereich
- 6. Die Randomizer-Klasse seit PHP 8.2
- 7. Schlüssel, Salts und Initialisierungsvektoren
- 8. Häufige Fehler bei eigener Zufallszahlen-Erzeugung
- 9. Zufallsquellen in PHP im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum Zufallsqualität sicherheitsrelevant ist
Nicht jede Anforderung an sichere Zufallszahlen in PHP ist gleich. Wer eine Würfelsimulation oder eine zufällige Sortierreihenfolge für eine Produktliste braucht, kann mit einem gewöhnlichen Pseudozufallsgenerator arbeiten, ohne dass Sicherheitsrisiken entstehen. Anders sieht es aus, sobald Zufallswerte als Passwort-Reset-Token, als Session-ID, als API-Schlüssel oder als Initialisierungsvektor für Verschlüsselung eingesetzt werden. In diesen Fällen muss ein Angreifer den erzeugten Wert praktisch unmöglich vorhersagen können, selbst wenn er viele frühere Ausgaben desselben Generators kennt.
Der Unterschied zwischen einem gewöhnlichen und einem kryptographisch sicheren Zufallsgenerator liegt im internen Algorithmus. Gewöhnliche Generatoren sind auf Geschwindigkeit und statistische Gleichverteilung optimiert, nicht auf Unvorhersehbarkeit gegenüber einem Angreifer. Sichere Zufallszahlen in PHP zu erzeugen bedeutet daher, gezielt die richtige Funktion für den jeweiligen Anwendungsfall zu wählen, statt reflexartig die erstbeste Zufallsfunktion aus der PHP-Dokumentation zu übernehmen. Dieser Artikel zeigt, welche Funktionen wirklich kryptographisch sicher sind und wie man sie korrekt einsetzt.
2. random_bytes und random_int: die kryptographisch sichere API
PHP bietet seit Version 7 zwei zentrale Funktionen für sichere Zufallszahlen: random_bytes(int $length) liefert eine beliebige Anzahl kryptographisch sicherer Zufallsbytes als Binärstring, random_int(int $min, int $max) liefert eine gleichverteilte Ganzzahl in einem festen Bereich. Beide Funktionen greifen intern auf einen sogenannten CSPRNG zu, einen Cryptographically Secure Pseudo-Random Number Generator, der von der Betriebssystemebene bereitgestellt wird, unter Linux typischerweise getrandom() oder /dev/urandom, unter Windows die CryptGenRandom-API.
Der entscheidende Vorteil dieser sicheren Zufallszahlen-Funktionen ist, dass sie ohne externe Bibliothek und ohne manuelle Seed-Verwaltung auskommen. Ein Entwickler muss sich nicht darum kümmern, woher die Entropie stammt, PHP delegiert das vollständig an das Betriebssystem. Beide Funktionen werfen eine Exception, wenn keine ausreichende Entropiequelle verfügbar ist, statt stillschweigend einen schwächeren Fallback zu verwenden, was für sicherheitskritischen Code ein wichtiges Verhalten ist. Genau diese Zuverlässigkeit macht random_bytes() und random_int() zur ersten Wahl für jede Anwendung, die sichere Zufallszahlen benötigt.
3. Warum rand() und mt_rand() für Security-Code ungeeignet sind
Die Funktionen rand() und mt_rand() erzeugen keine sicheren Zufallszahlen, weil ihr zugrunde liegender Algorithmus, der Mersenne-Twister, deterministisch ist. Kennt ein Angreifer eine ausreichende Anzahl aufeinanderfolgender Ausgaben, kann er den internen Zustand des Generators rekonstruieren und alle zukünftigen Werte vorhersagen. Dieses Verhalten ist in der Kryptographie-Fachliteratur seit Langem dokumentiert und wurde in der Praxis bereits gegen reale Anwendungen ausgenutzt, etwa um Passwort-Reset-Tokens zu erraten, die naiverweise mit mt_rand() erzeugt wurden.
Ein weiteres Problem: mt_rand() wird standardmäßig mit der Systemzeit oder einem festen Startwert initialisiert (geseedet), wenn kein expliziter Seed übergeben wird. Diese Initialisierung kann in bestimmten Umgebungen, etwa bei parallel gestarteten Prozessen zur exakt gleichen Sekunde, zu identischen Zufallsfolgen führen. Für sichere Zufallszahlen darf ein solcher Mechanismus niemals verwendet werden, unabhängig davon, wie unwahrscheinlich ein konkreter Angriff im Einzelfall erscheinen mag. Die einzige belastbare Faustregel lautet: Sobald ein Zufallswert eine sicherheitsrelevante Funktion hat, kommt ausschließlich random_bytes() oder random_int() infrage.
<?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. Sichere Tokens und IDs in der Praxis erzeugen
Der häufigste praktische Anwendungsfall für sichere Zufallszahlen ist die Erzeugung von Tokens, etwa für Passwort-Reset-Links, API-Schlüssel oder Einladungscodes. Die Faustregel für die Länge: Mindestens 16 Byte Rohentropie (128 Bit) für kurzlebige Tokens, 32 Byte (256 Bit) für langlebige oder besonders sensible Werte wie API-Schlüssel. Die Kodierung sollte URL-sicher sein, wenn der Token in einer URL übertragen wird, weshalb sich Base64url gegenüber klassischem Base64 oder Hexadezimal oft als kompakter und praktikabler erweist.
Bei Passwort-Reset-Tokens ist zusätzlich wichtig, den Token niemals im Klartext in der Datenbank zu speichern, sondern nur seinen Hash, üblicherweise mit hash('sha256', $token). So bleibt der eigentliche Token auch bei einem Datenbank-Leak nutzlos für einen Angreifer, weil er den ursprünglichen Wert nicht aus dem gespeicherten Hash zurückrechnen kann. Diese Kombination aus sicheren Zufallszahlen zur Erzeugung und Hashing zur Speicherung ist der Industriestandard für token-basierte 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. Zufallszahlen in einem festen Wertebereich
Für Einmalpasswörter (OTPs), Verifizierungscodes oder Lostrommel-Ziehungen braucht man häufig sichere Zufallszahlen innerhalb eines konkreten Bereichs, zum Beispiel eine sechsstellige Zahl zwischen 100000 und 999999. Genau dafür ist random_int($min, $max) gedacht: Die Funktion garantiert eine gleichverteilte Auswahl innerhalb der Grenzen, ohne den bekannten Modulo-Bias-Fehler, der bei naiven Implementierungen mit random_bytes() und anschließendem %-Operator auftreten kann.
Der Modulo-Bias entsteht, wenn der Wertebereich des Zufallsgenerators nicht exakt durch die gewünschte Bereichsgröße teilbar ist, wodurch manche Ergebniswerte minimal häufiger auftreten als andere. Bei kryptographischen Anwendungen, etwa der Auswahl eines zufälligen Elements aus einem Array mit Schlüsselmaterial, kann dieser scheinbar kleine statistische Fehler tatsächlich ausnutzbar sein. random_int() vermeidet dieses Problem intern durch sogenanntes Rejection Sampling und ist deshalb immer der direkten Modulo-Rechnung mit random_bytes() vorzuziehen, wenn eine Ganzzahl in einem Bereich benötigt wird.
<?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. Die Randomizer-Klasse seit PHP 8.2
Seit PHP 8.2 gibt es mit der Klasse \Random\Randomizer eine objektorientierte API, die den Umgang mit sicheren Zufallszahlen und deterministischen Zufallsfolgen sauber trennt. Der Konstruktor akzeptiert eine sogenannte Engine, die entweder \Random\Engine\Secure für kryptographisch sicheren Zufall oder \Random\Engine\Mt19937 für einen reproduzierbaren, geseedeten Zufall verwendet, etwa für deterministische Tests. Diese explizite Trennung macht sofort sichtbar, welche Art von Zufall an einer bestimmten Stelle im Code verwendet wird, statt es implizit über die aufgerufene Funktion zu erraten.
Die Randomizer-Klasse bietet zusätzlich komfortable Methoden wie getBytes(), getInt(), shuffleArray() und pickArrayKeys(), die intern konsistent die gewählte Engine verwenden. Für die meisten sicherheitsrelevanten Fälle bleibt random_bytes() und random_int() weiterhin die einfachste Wahl, aber sobald ein Projekt sowohl reproduzierbare Test-Zufallsfolgen als auch echte sichere Zufallszahlen benötigt, etwa für unterschiedliche Testszenarien, ist die Randomizer-Klasse die sauberere Architekturlösung.
<?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. Schlüssel, Salts und Initialisierungsvektoren
Kryptographische Operationen wie symmetrische Verschlüsselung mit AES-GCM benötigen einen Initialisierungsvektor (IV), der niemals wiederverwendet werden darf, sowie einen Schlüssel, der ausreichend Entropie besitzt. Beide Werte müssen aus sicheren Zufallszahlen stammen, sonst untergräbt eine schwache Zufallsquelle die gesamte kryptographische Konstruktion, selbst wenn der Verschlüsselungsalgorithmus selbst als sicher gilt. Für AES-GCM liefert random_bytes(12) einen passenden 96-Bit-IV, für einen AES-256-Schlüssel liefert random_bytes(32) die benötigten 256 Bit.
Auch Salts für Passwort-Hashing, etwa bei einer eigenen HMAC-Konstruktion außerhalb von password_hash(), müssen aus sicheren Zufallszahlen erzeugt werden. Ein vorhersagbarer Salt erlaubt Angreifern, vorab berechnete Rainbow-Tables gegen bekannte Salt-Muster zu erstellen, wodurch der eigentliche Zweck des Salts, jede Passwort-Hash-Berechnung individuell zu machen, komplett verfehlt wird. Bei modernen Passwort-Hashing-Funktionen wie password_hash() mit Argon2id übernimmt PHP diese Salt-Generierung intern bereits korrekt, weshalb ein manueller Umgang mit Salts nur bei eigenen, nicht standardmäßigen kryptographischen Konstruktionen nötig ist.
<?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. Häufige Fehler bei eigener Zufallszahlen-Erzeugung
Ein wiederkehrender Fehler ist die Verwendung von uniqid() als vermeintliche Quelle für sichere Zufallszahlen. uniqid() basiert auf der aktuellen Systemzeit in Mikrosekunden und ist damit in engen Grenzen vorhersagbar, insbesondere wenn ein Angreifer den ungefähren Zeitpunkt der Generierung kennt. Auch die Kombination aus uniqid() mit dem Parameter more_entropy: true ändert daran grundsätzlich nichts, weil die zusätzliche Entropie aus lcg_value() stammt, einem weiteren nicht kryptographisch sicheren Generator.
Ein zweiter Fehler ist die Reduktion der Token-Länge aus Bequemlichkeit, etwa nur 8 statt 32 Byte Rohentropie, um kürzere URLs zu erhalten. Bei einer zu kurzen Token-Länge wird ein Brute-Force-Angriff praktisch durchführbar, besonders wenn kein Rate-Limiting auf dem Endpunkt existiert, der den Token prüft. Ein dritter, oft übersehener Fehler betrifft die Wiederverwendung von Initialisierungsvektoren bei symmetrischer Verschlüsselung: Selbst wenn der IV selbst aus sicheren Zufallszahlen stammt, bricht die Sicherheit von AES-GCM zusammen, sobald derselbe IV zweimal mit demselben Schlüssel verwendet wird.
9. Zufallsquellen in PHP im Vergleich
Die folgende Tabelle stellt die wichtigsten PHP-Funktionen zur Zufallszahlen-Erzeugung gegenüber und zeigt, für welchen Anwendungsfall sie jeweils geeignet oder ungeeignet sind.
| Funktion | Kryptographisch sicher | Typischer Einsatz | Bewertung |
|---|---|---|---|
| rand() / mt_rand() | Nein | Spiele, Simulationen, UI-Zufall | Niemals für Tokens oder Schlüssel |
| uniqid() | Nein | Eindeutige Bezeichner, kein Sicherheitsbezug | Vorhersagbar über Systemzeit |
| random_bytes() | Ja | Tokens, Schlüssel, IVs | Erste Wahl für Rohentropie |
| random_int() | Ja | OTPs, Codes, Bereichsauswahl | Kein Modulo-Bias |
| Randomizer (Secure) | Ja | Objektorientierte APIs, Array-Shuffle | Explizite Engine-Wahl seit 8.2 |
Die Tabelle macht deutlich: Sobald ein Zufallswert eine Sicherheitsfunktion erfüllt, kommen nur die unteren drei Zeilen infrage. rand(), mt_rand() und uniqid() bleiben legitime Werkzeuge für nicht sicherheitsrelevante Anwendungsfälle, dürfen aber niemals in denselben Codepfad wie sichere Zufallszahlen geraten, etwa durch Copy-Paste aus älterem Code.
Mironsoft
PHP-Sicherheitsberatung und Kryptographie-Reviews
Noch mt_rand() oder uniqid() im Sicherheitscode?
Wir finden unsichere Zufallsquellen in bestehendem PHP-Code, ersetzen sie durch random_bytes, random_int und die Randomizer-Klasse und prüfen Token- und Verschlüsselungslogik auf Herz und Nieren.
Code-Audit
Gezielte Suche nach mt_rand, rand und uniqid in sicherheitsrelevantem Code
Token-Design
Sichere Token-Factories für Passwort-Reset, API-Keys und Einladungscodes
Kryptographie-Review
Prüfung von Schlüssel-, IV- und Salt-Generierung in Verschlüsselungscode
10. Zusammenfassung
Sichere Zufallszahlen in PHP zu erzeugen ist kein Detail, sondern eine Grundvoraussetzung für jede Anwendung, die Tokens, Schlüssel oder Sitzungswerte verarbeitet. random_bytes() und random_int() sind seit PHP 7 die richtige Wahl für nahezu jeden sicherheitsrelevanten Fall, weil sie auf einem kryptographisch sicheren Betriebssystem-Generator basieren und bei fehlender Entropie kontrolliert eine Exception werfen, statt lautlos einen schwächeren Fallback zu nutzen. Die Randomizer-Klasse ab PHP 8.2 ergänzt diese Funktionen um eine objektorientierte, explizit engine-basierte API.
rand(), mt_rand() und uniqid() bleiben für nicht sicherheitsrelevante Anwendungsfälle völlig legitim, dürfen aber niemals dort landen, wo Vorhersagbarkeit ein Risiko darstellt. Wer diese Trennung konsequent im Code durchsetzt, etwa durch statische Analyse-Regeln, die mt_rand() in sicherheitskritischen Verzeichnissen verbieten, reduziert eine ganze Klasse von Schwachstellen von vornherein.
Sichere Zufallszahlen in PHP — Das Wichtigste auf einen Blick
Rohentropie
random_bytes() für Tokens, Schlüssel und Initialisierungsvektoren, mindestens 16 bis 32 Byte.
Ganzzahlen in Bereichen
random_int($min, $max) ohne Modulo-Bias für OTPs und Verifizierungscodes.
Niemals dafür
rand(), mt_rand() und uniqid() sind für sicherheitsrelevante Werte immer ungeeignet.
Seit PHP 8.2
Die Randomizer-Klasse trennt sicheren und deterministischen Zufall explizit über die Engine.