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() und mt_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, ...).