Sichere Zufallszahlen in PHP erzeugen
AI generated
<?php
8.4
PHP · Security · Kryptographie · Tokens
Sichere Zufallszahlen in PHP erzeugen
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.

16 Min. Lesezeit random_bytes · random_int · Randomizer · CSPRNG PHP 8.2+

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.

11. FAQ: Sichere Zufallszahlen in PHP

1Was unterscheidet random_bytes von rand()?
random_bytes nutzt einen kryptographisch sicheren Betriebssystem-Generator, rand() basiert auf einem vorhersagbaren Algorithmus.
2Ist mt_rand() immer unsicher?
Für Spiele oder Sortierung ist es unbedenklich, für Tokens und Schlüssel dagegen immer ungeeignet.
3Warum random_int statt Modulo?
random_int vermeidet den Modulo-Bias intern durch Rejection Sampling und liefert eine echte Gleichverteilung.
4Wie lang sollte ein Token sein?
Mindestens 16 Byte für kurzlebige, 32 Byte für langlebige oder sensible Tokens.
5Warum ist uniqid() unsicher?
Basiert auf der Systemzeit in Mikrosekunden und ist dadurch in engen Grenzen vorhersagbar.
6Was ist die Randomizer-Klasse?
Eine objektorientierte API seit PHP 8.2, die sichere und deterministische Zufalls-Engines explizit trennt.
7Darf ein IV wiederverwendet werden?
Nein, niemals mit demselben Schlüssel, sonst bricht die Sicherheit von AES-GCM vollständig.
8Muss ich Salts selbst generieren?
Nein, password_hash mit Argon2id oder bcrypt übernimmt die Salt-Generierung intern korrekt.
9Was passiert ohne Entropiequelle?
random_bytes wirft eine Exception, statt lautlos einen schwächeren Wert zu liefern.
10Eignen sich beide für Passwort-Generatoren?
Ja, solange random_int statt einer eigenen Modulo-Rechnung für die Zeichenauswahl genutzt wird.