Kurz gesagt: Sichere Tokens erzeugen Sie in PHP mit random_bytes(), etwa 32 Bytes, die Sie mit bin2hex() oder Base64-URL in Text umwandeln. Für Zufallszahlen in einem Bereich gibt es random_int(), für Codes aus einem festen Zeichenvorrat seit PHP 8.3 Random\Randomizer::getBytesFromString(). Tokens speichern Sie als Hash, begrenzen ihre Gültigkeit, nutzen sie nur einmal und vergleichen sie mit hash_equals().
Ein Link, der zu leicht zu erraten ist
„Passwort vergessen?“ Ein Klick, und das System verschickt einen Link mit einem langen Code. Wer den Link kennt, kann das Passwort setzen. Genau deshalb muss dieser Code unvorhersehbar sein. Ist er es nicht, braucht ein Angreifer weder das Postfach noch das alte Passwort, nur genug Versuche.
In Prüfungen von PHP-Anwendungen finden sich erstaunlich oft Tokens, die aus der Uhrzeit, der Benutzer-ID oder einem einfachen Zufallszahlengenerator gebaut sind. Sie sehen lang und zufällig aus, sind es aber nicht. Dasselbe gilt für API-Schlüssel, Einladungslinks, Freigabelinks für Dokumente, Bestätigungscodes und CSRF-Tokens.
Das Risiko ist handfest: Ein vorhersagbarer Reset-Link erlaubt die Übernahme jedes Kontos, dessen E-Mail-Adresse bekannt ist, ohne dass der Inhaber etwas bemerkt, bis er sich selbst nicht mehr anmelden kann.
PHP bietet seit Version 7.0 eine einfache und sichere Lösung. Sie muss nur konsequent genutzt werden.
random_bytes in PHP: Woher sichere Zufallswerte kommen
random_bytes() liefert laut PHP-Handbuch kryptografisch sichere Zufallsbytes, die sich auch für langlebige Geheimnisse wie Schlüssel eignen. Die Funktion nutzt die Zufallsquelle des Betriebssystems, unter Linux zum Beispiel getrandom(), unter Windows die CNG-Schnittstelle. Findet sie keine geeignete Quelle, wirft sie seit PHP 8.2 eine Random\RandomException.
Die Übersicht zeigt, welche Funktionen für Geheimnisse taugen:
| Funktion | Für Tokens geeignet? | Hinweis |
|---|---|---|
random_bytes() |
ja | sichere Bytes, seit PHP 7.0 |
random_int() |
ja | sichere Zahl in einem Bereich, seit PHP 7.0 |
Random\Randomizer mit Secure-Engine |
ja | Secure ist laut Handbuch die Standard-Engine |
rand(), mt_rand() |
nein | vorhersagbar, nur für nicht sicherheitsrelevante Zwecke |
uniqid() |
nein | zeitbasiert, für Eindeutigkeit gedacht |
md5(time()), sha1($id) |
nein | Eingabe erratbar, Hash ändert daran nichts |
str_shuffle(), array_rand() |
nein | nutzen keinen sicheren Generator |
Tokens erzeugen: Länge und Kodierung
32 zufällige Bytes entsprechen 256 Bit und sind für fast jeden Zweck mehr als genug. Für die Übertragung in Links oder Headern muss das Token in Text umgewandelt werden. Zwei Varianten sind üblich:
<?php
declare(strict_types=1);
// Hex: 64 Zeichen, nur 0-9 und a-f
$tokenHex = bin2hex(random_bytes(32));
// Base64-URL: 43 Zeichen, sicher in URLs ohne weitere Kodierung
$tokenUrl = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
// Zufallszahl in einem Bereich, etwa für eine Auswahl
$index = random_int(0, 9);
// Ab PHP 8.3: sechsstelliger Zahlencode, z. B. für eine Bestätigung per SMS
$randomizer = new \Random\Randomizer();
$code = $randomizer->getBytesFromString('0123456789', 6);
Kurze Zahlencodes sind nur dann vertretbar, wenn die Anzahl der Versuche streng begrenzt ist und der Code nach wenigen Minuten verfällt. Für Links, die per E-Mail verschickt werden, nehmen Sie immer das lange Token.
Für API-Schlüssel, die Partner oder eigene Systeme dauerhaft nutzen, haben sich zwei Ergänzungen bewährt. Ein kurzes, nicht geheimes Präfix vor dem Zufallsteil, etwa für Umgebung oder Zweck, erleichtert das Erkennen in Protokollen und bei Funden in Code-Repositories. Und der Schlüssel wird nur einmal bei der Erstellung angezeigt; danach kennt das System nur noch seinen Hash. Geht ein Schlüssel verloren, wird er ersetzt, nicht wiederhergestellt.
Vorsicht bei selbst gebauten Tokens, die Informationen enthalten, etwa Benutzer-ID und Ablaufzeit in Base64. Kodierung ist keine Sicherung: Jeder kann solche Tokens lesen und verändern. Soll ein Token Daten tragen, muss es mit einem geheimen Schlüssel signiert sein, zum Beispiel per hash_hmac('sha256', ...), und die Signatur wird mit hash_equals() geprüft. Für aufwendigere Fälle sind geprüfte Bibliotheken eigenen Konstruktionen vorzuziehen.
Tokens speichern, prüfen und verfallen lassen
Ein sicher erzeugtes Token ist nur die halbe Miete. Mindestens ebenso wichtig ist, wie die Anwendung damit umgeht. Ein bewährtes Vorgehen für Reset- und Einladungslinks:
- Nur den Hash speichern: In der Datenbank steht
hash('sha256', $token), das Token selbst geht nur per Link an den Nutzer. - Gültigkeit begrenzen: Ablaufzeit mitspeichern und beim Einlösen prüfen.
- Einmal verwenden: Nach erfolgreicher Nutzung den Eintrag löschen oder als verbraucht markieren.
- Bei Änderungen entwerten: Nach einer Passwortänderung alle offenen Reset-Tokens des Kontos löschen.
- Konstant vergleichen: Wo ein Token direkt mit einem gespeicherten Wert verglichen wird,
hash_equals()nutzen, mit dem gespeicherten Wert als erstem Parameter.
<?php
// Erzeugen
$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'),
];
// $token nur im Link versenden, $eintrag speichern
// Einlösen
$gesucht = hash('sha256', (string) ($_GET['token'] ?? ''));
// Eintrag per Prepared Statement über token_hash suchen,
// Ablauf prüfen, danach Eintrag sofort löschen
Begrenzen Sie außerdem die Zahl der Reset-Anfragen pro Konto und Zeitraum, damit die Funktion nicht zum massenhaften Versand von E-Mails missbraucht werden kann. Die Antwort auf eine Reset-Anfrage sollte zudem nicht verraten, ob die angegebene Adresse überhaupt ein Konto hat.
Ein schneller Hash wie SHA-256 genügt hier, weil das Token selbst schon lang und zufällig ist. Für Passwörter, die Menschen wählen, gilt das nicht; dort braucht es langsame Verfahren, wie Passwörter in PHP sicher speichern erklärt.
Tokens gehören nicht in Logdateien. Prüfen Sie, ob Ihr Webserver vollständige URLs mit Query-Parametern protokolliert, und ob Fehlerberichte Anfragedaten mitschicken.
Nach einem Vorfall: Tokens und Schlüssel erneuern
Wer Code auf einem Server ausführen konnte, hatte in der Regel auch Zugriff auf gespeicherte Tokens, API-Schlüssel und Sitzungsdaten. Besteht nach PHP2Shell oder einem anderen Vorfall der Verdacht auf einen Einbruch, sollten Sie deshalb nicht nur Passwörter, sondern auch langlebige Tokens und Schlüssel erneuern: API-Zugänge, Signaturschlüssel für Cookies oder Links, Zugänge zu Zahlungs- und Versanddiensten.
Sessions sind ein Sonderfall: PHP erzeugt Session-IDs selbst. Wie Sie deren Einstellungen absichern und Sessions nach einem Vorfall beenden, beschreibt der Ratgeber Sessions in PHP absichern. Für Formular-Tokens gegen gefälschte Anfragen siehe CSRF-Schutz in PHP-Anwendungen. Weitere eingebaute Werkzeuge stellt Eingebaute Sicherheitsfunktionen von PHP vor.
Wie hilft PHP SOS konkret?
- Wir finden in Ihrer Anwendung alle Stellen, an denen Tokens, Codes und Schlüssel erzeugt werden, und ersetzen unsichere Quellen.
- Wir prüfen Speicherung, Ablauf und Einmalverwendung von Reset-, Einladungs- und Freigabelinks.
- Wir sorgen dafür, dass Tokens nicht in Logs, Fehlerberichten oder Analysewerkzeugen landen.
- Nach einem Vorfall planen wir mit Ihnen den geordneten Austausch von Tokens und Schlüsseln.
Die Prüfung eigener PHP-Software bieten wir als Sicherheitsaudit und Penetrationstest an.
Häufige Fragen
Warum ist mt_rand für Tokens ungeeignet?
mt_rand und rand sind für Statistik und Spiele gedacht, nicht für Geheimnisse. Ihre Ausgaben lassen sich unter Umständen vorhersagen. Für alles, was ein Angreifer nicht erraten darf, nutzen Sie random_bytes oder random_int.
Wie lang sollte ein Token sein?
Für Links zum Zurücksetzen von Passwörtern, Einladungen oder API-Schlüssel sind 32 zufällige Bytes eine solide Wahl, als Hex-Text 64 Zeichen. 16 Bytes, also 128 Bit, sind eine verbreitete Untergrenze.
Ist uniqid ein sicherer Token?
Nein. uniqid basiert auf der aktuellen Zeit und ist für Eindeutigkeit gedacht, nicht für Geheimhaltung. Auch Kombinationen wie md5 von Zeit oder Benutzer-ID sind vorhersagbar.
Muss ich Reset-Tokens in der Datenbank verschlüsseln?
Speichern Sie statt des Tokens einen Hash davon, zum Beispiel mit SHA-256. Wer die Datenbank liest, kann damit keine gültigen Links bauen. Weil das Token selbst schon lang und zufällig ist, braucht es kein langsames Verfahren wie bei Passwörtern.
Was passiert, wenn das System keine Zufallsquelle findet?
random_bytes und random_int werfen seit PHP 8.2 eine Random\RandomException. Fangen Sie diesen Fall ab und brechen Sie den Vorgang ab, statt auf eine schwächere Quelle auszuweichen.