PHP: Wie erzeugt man einen Zufallsstring?
Einen zufälligen String braucht man immer wieder. Für einen Dateinamen, für einen Cache-Key oder für ein Token. Je nach Verwendungszweck sind dabei aber ganz unterschiedliche Dinge zu beachten.
Die wichtigste Frage stellst du dir vorab: Darf jemand den String erraten? Geht es nur um einen eindeutigen Dateinamen, ist die Antwort egal. Geht es um einen Passwort-Reset-Link oder ein Session-Token, hängt daran die Sicherheit deiner Anwendung.
Der einfache Fall
Der kürzeste Weg zu einem sicheren Zufallsstring führt über random_bytes und bin2hex:
<?php
echo bin2hex(random_bytes(16));
?>
Ausgabe:
71649e32bddbae7ad9149822227aa85e
random_bytes() liefert die angeforderte Anzahl an Bytes aus der
kryptografisch sicheren Quelle des Betriebssystems. bin2hex() wandelt diese
in lesbare Hexadezimalzeichen um. Aus 16 Bytes werden dabei 32 Zeichen, denn
jedes Byte braucht zwei Zeichen.
Das Ergebnis besteht nur aus 0-9 und a-f. Für die meisten Zwecke ist das
völlig in Ordnung und du brauchst nicht weiterzulesen.
Ein eigenes Alphabet
Willst du bestimmen welche Zeichen vorkommen dürfen, ziehst du die Zeichen einzeln mit random_int:
<?php
function zufallsString(int $laenge, string $alphabet = '0123456789abcdefghijklmnopqrstuvwxyz'): string {
$max = strlen($alphabet) - 1;
$out = '';
for ($i = 0; $i < $laenge; $i++) {
$out .= $alphabet[random_int(0, $max)];
}
return $out;
}
echo zufallsString(12); // z.B. 7t3jyvhkma33
?>
random_int() ist die sichere Variante von rand() und liefert eine Zahl im
angegebenen Bereich — ohne die Verzerrung, um die es gleich noch geht.
Warum nicht rand() oder mt_rand()?
Weil sich deren Ausgabe reproduzieren lässt. Beide Funktionen erzeugen keine echten Zufallszahlen, sondern eine Folge, die vollständig von einem Startwert (dem Seed) abhängt. Setzt man denselben Seed, kommt dieselbe Folge heraus:
1. Lauf mit Seed 42: 637542, 126067, 286876, 846414, 890026
2. Lauf mit Seed 42: 637542, 126067, 286876, 846414, 890026
identisch: true
Für einen Angreifer heißt das: Wer den Seed kennt oder aus beobachteten Werten ableiten kann, kennt alle weiteren Werte. Für Testdaten ist genau diese Eigenschaft übrigens praktisch, weil du einen Fehler damit reproduzieren kannst. Für Token ist sie tödlich.
Die Falle: der Modulo-Bias
Sehr verbreitet ist folgende Zeile, um ein zufälliges Byte auf ein Alphabet abzubilden:
<?php
// Achtung, diese Variante ist verzerrt!
$zeichen = $alphabet[ord(random_bytes(1)) % strlen($alphabet)];
?>
Das Problem hat nichts mit der Zufallsquelle zu tun, sondern mit dem Rest der
Division. Ein Byte hat 256 mögliche Werte. Bei einem Alphabet mit 10 Zeichen
ist 256 / 10 = 25 Rest 6. Die ersten sechs Zeichen des Alphabets bekommen
also eine Chance mehr als die übrigen vier.
Das ist keine graue Theorie, das kann man messen. 600.000 Ziehungen, Alphabet
0123456789:
0: 10.229 %
1: 10.119 %
2: 10.167 %
3: 10.159 %
4: 10.096 %
5: 10.126 %
6: 9.810 %
7: 9.778 %
8: 9.755 %
9: 9.760 %
Der Sprung liegt exakt zwischen der 5 und der 6, also genau dort wo die
Rechnung ihn vorhersagt. Zum Vergleich dieselbe Anzahl Ziehungen mit
random_int(0, 9):
0: 10.085 %
1: 9.997 %
...
8: 10.008 %
9: 9.986 %
Hier gibt es keinen solchen Block, die Abweichungen streuen zufällig. 9,76 %
statt 10 % klingt nach wenig. Bei einem Token bedeutet es aber, dass ein
Angreifer seine Rateversuche sinnvoll sortieren kann. random_int() nimmt dir
diese Sorge ab, weil es intern so lange neu zieht bis der Wert im gewünschten
Bereich verzerrungsfrei ist.
uniqid() ist kein Zufall
Ein häufiges Missverständnis. uniqid() erzeugt keine Zufallswerte, sondern
formatiert die aktuelle Zeit. Drei Aufrufe hintereinander:
6a91e83188307
6a91e8318830b
6a91e8318830c
Die Werte unterscheiden sich nur an den letzten Stellen und zählen hoch. Als Kennung innerhalb eines Requests ist das brauchbar, als Token nicht.
Ähnlich sieht es bei str_shuffle aus. Die Funktion mischt die übergebenen Zeichen, jedes Zeichen kommt danach also genau einmal vor. Für einen Zufallsstring aus einem Alphabet ist sie damit unabhängig von der Zufallsquelle die falsche Wahl.
Zusammenfassung
- Für Token, Links und Schlüssel:
random_bytes()bzw.random_int(). - Für alles andere reicht
bin2hex(random_bytes(16))genauso. rand()undmt_rand()sind reproduzierbar und für Sicherheitszwecke ungeeignet.- Bilde Bytes nie per
%auf ein Alphabet ab, das verzerrt die Verteilung. uniqid()ist eine Uhr,str_shuffle()eine Permutation. Beides ist kein Zufallsgenerator.
Hinweis zu Netcup (Werbung)
Der deutsche Hoster Netcup bietet unter anderem günstige und zugleich leistungsstarke Webhosting Pakete, KVM-basierte Root Server und dezidierte Server an. Mit unseren Gutscheincodes kannst du noch mehr Geld sparen (6€ bei deiner ersten Bestellung, 30% Rabatt auf alle KVM-basierten Root Server, ...).