Passwort-Reset für kleine PHP-Anwendungen - Recovery darf nicht leichter sein als Login
Ein Formular zum Zurücksetzen eines Passworts wirkt oft wie eine kleine Funktion: E-Mail-Adresse annehmen, Link versenden und den Benutzer ein neues Passwort wählen lassen. Der Link übernimmt jedoch vorübergehend die Aufgabe der Authentifizierung. Wenn sich dieser leisere Seiteneingang leichter beobachten, erraten, wiederverwenden oder missbrauchen lässt als die Login-Seite, kann auch ein starker Login-Ablauf das nicht ausgleichen.
Die entscheidende Designfrage ist nicht, ob das Formular eine E-Mail versendet. Entscheidend ist, ob jeder Übergang einen eng begrenzten Sicherheitsvertrag einhält: Die öffentliche Antwort verrät kein Konto, der Link enthält ein unvorhersehbares und kurzlebiges Bearer Secret, der Server speichert und verwendet dieses Secret sorgfältig, und die Passwortänderung schließt den Recovery-Vorgang sauber ab. Dieser Artikel untersucht diese Invarianten für eine kleine native PHP-Anwendung. Er ist ein Prüfmodell, kein vollständiger Controller zum direkten Einsatz in Production.
Recovery als weiteren Authentifizierungspfad behandeln
Wer sein Passwort vergessen hat, kann die Kontrolle über dieses Passwort nicht nachweisen. Recovery ersetzt es deshalb durch ein anderes Signal, meistens durch den Zugriff auf das E-Mail-Postfach des Kontos. Die PortSwigger Web Security Academy beschreibt Password Reset aus diesem Grund als inhärent sensibel: Der Mechanismus kann einen Weg an der normalen Passwortauthentifizierung vorbei eröffnen.
Recovery über einen E-Mail-Link ist praktisch, hat aber Grenzen bei der Assurance. Ein kompromittiertes Postfach, eine weitergeleitete Nachricht, ein offener Browser-Verlauf oder ein geleaktes Anwendungsprotokoll können den Link offenlegen. Der Zugriff auf ein Postfach zu einem bestimmten Zeitpunkt beweist außerdem nicht die bürgerliche Identität einer Person. Konten mit höherem Risiko können stärkere Recovery-Verfahren benötigen, während eine kleine Community-Website E-Mail-Recovery nach Dokumentation des Trade-offs vertretbar akzeptieren kann. „Sicher“ bedeutet hier, vermeidbare Fehler zu reduzieren, nicht E-Mail in ein unfehlbares Identitätssystem zu verwandeln.
Den Ablauf vor der Code-Auswahl abbilden
Ein vertretbarer Ablauf lässt sich in vier Phasen aufteilen:
- Request: einen Konto-Identifier annehmen und eine neutrale Antwort zurückgeben.
- Issue: falls das Konto existiert und die Richtlinie es erlaubt, ein begrenztes Token erzeugen und eine vertrauenswürdige HTTPS-URL per E-Mail senden.
- Present: das Token empfangen, seinen Zustand prüfen und das Formular für das neue Passwort anzeigen, ohne das Credential anderweitig preiszugeben.
- Consume: das Token erneut validieren, das Passwort ersetzen, das Token als verwendet markieren, über bestehende Sessions entscheiden und den Benutzer benachrichtigen.
Diese Trennung macht Failure Modes sichtbar. Ein Request-Endpoint kann registrierte Adressen verraten. Die Issue-Phase kann aus einem vom Angreifer kontrollierten Host Header einen vergifteten Link erzeugen. Eine Presentation-Seite kann ihren Query String an Drittanbieter-Ressourcen weitergeben. Die Consumption-Phase kann zwei gleichzeitigen Requests erlauben, dasselbe Token wiederzuverwenden. Sicherheit ist eine Eigenschaft der gesamten Folge, nicht nur des zufälligen Strings.
Die Request-Antwort bewusst informationsarm gestalten
Das OWASP Forgot Password Cheat Sheet empfiehlt dieselbe öffentliche Nachricht für existierende und nicht existierende Konten. Eine geeignete Antwort lautet etwa: „Wenn ein Konto zu dieser Adresse passt, werden Anweisungen zum Zurücksetzen gesendet.“ Statuscode, Wortlaut, Redirect und sichtbare Seitenstruktur sollten nicht davon abhängen, ob ein Datensatz gefunden wurde.
Auch das Timing verdient Aufmerksamkeit. Ein schneller Datenbankfehler mit sofortiger Antwort kann sich von einem erfolgreichen Lookup unterscheiden, der ein Token erzeugt und auf einen Mail Provider wartet. Eine vollkommen konstante Antwortzeit über ein Netzwerk ist unrealistisch, aber ein offensichtlicher Early Exit sollte vermieden werden. Eine praktische Architektur legt berechtigte E-Mail-Arbeit in einer Queue ab und antwortet nach vergleichbarem Anwendungsaufwand; der E-Mail-Versand erfolgt anschließend außerhalb des Requests. Verteilungen sollten gemessen werden, statt anzunehmen, ein Fixed Sleep habe jeden Side Channel beseitigt.
Neutrale Antworten stoppen keinen Missbrauch. Mehrschichtiges Throttling nach Quelle und Konto-Identifier, die Überwachung von Spitzen und gegebenenfalls eine zusätzliche Challenge können sinnvoll sein, wenn das Risiko deren Kosten für Barrierefreiheit und Datenschutz rechtfertigt. Genaue Grenzen hängen von normalem Traffic und der Mail-Latenz ab. Eine unbegründete Zahl zu kopieren ist schwächer, als den Service zu beobachten. OWASP warnt außerdem davor, ein Konto allein wegen angeforderter Resets zu sperren: Kennt ein Angreifer eine Adresse, könnte er Recovery sonst in Denial of Service verwandeln.
Ein opakes und begrenztes Credential ausstellen
Ein Reset-Token sollte von einem kryptografisch sicheren Zufallszahlengenerator erzeugt, mit genau einem Benutzer verknüpft, nach einer dokumentierten kurzen Frist ablaufen und nur einmal funktionieren. Es sollte keine E-Mail-Adresse, User ID, keinen Timestamp und keine Role enthalten, die der Browser interpretieren müsste. Opaque Werte halten Authorization-Entscheidungen auf dem Server und geben Angreifern keine nützliche Struktur.
Laut offizieller PHP-Dokumentation erzeugt random_bytes() gleichmäßig ausgewählte, kryptografisch sichere Zufallsbytes. Ein kompakter Ausschnitt für die Ausstellung kann deshalb so aussehen:
<?php
$token = bin2hex(random_bytes(32));
$tokenDigest = hash('sha256', $token);
// Store $tokenDigest with user_id, expires_at, and unused state.
// Send $token only in the HTTPS reset link.
Zweiunddreißig Zufallsbytes enthalten vor der Kodierung 256 zufällige Bits; die hexadezimale Darstellung macht daraus 64 druckbare Zeichen. Diese Tatsache macht das gesamte System nicht „256-Bit-sicher“. Postfächer, Anwendungsfehler, Logging und Token-Lifecycle-Kontrollen können wesentlich schwächer als der Generator sein.
Gespeichert werden sollte der Digest und nicht das rohe Bearer Token. Kommt ein Request zurück, wird aus dem übermittelten Wert ein Digest gebildet und der aktive Datensatz anhand dieses Digests gesucht. Dieses Muster begrenzt die unmittelbare Offenlegung des Tokens, falls nur die Reset-Tabelle gelesen wird. Ein schneller SHA-256-Digest eignet sich hier für ein maschinell erzeugtes Token mit hoher Entropie; er ist kein Ersatz für langsames Password Hashing, da von Benutzern gewählte Passwörter erraten werden können.
Die Datenbank benötigt weiterhin Constraints und Lifecycle-Regeln: einen Ablaufzeitpunkt, einen unbenutzten Zustand, die Benutzerzuordnung, Cleanup und eine klare Richtlinie für ältere Tokens, wenn ein neues ausgestellt wird. Die Base URL darf nicht stillschweigend aus einem nicht vertrauenswürdigen Host Header erzeugt werden. OWASP empfiehlt einen fest konfigurierten oder per Allowlist geprüften Host, und der Link muss HTTPS verwenden. Er sollte aus vertrauenswürdiger Anwendungskonfiguration entstehen.
Verhindern, dass die Reset-Seite ihr Credential verteilt
Ein Token in einem Query String kann im Browser-Verlauf, in Server Access Logs, Screenshots, kopiertem Text oder Referrer-Daten erscheinen. Ein Teil dieses Risikos gehört zu einer E-Mail-URL, doch die Reset-Seite sollte es nicht vergrößern. Analytics, Werbeskripte, Remote Fonts, Social Widgets und nicht notwendige externe Links gehören nicht auf diese Seite. Der Zugriff auf Logs sollte eng begrenzt und ihre Aufbewahrung definiert werden, statt sie als harmloses Nebenprodukt zu behandeln.
OWASP empfiehlt eine No-Referrer-Richtlinie für die Reset-Seite. Laut MDN-Referenz zu Referrer-Policy unterdrückt dieser Response Header Referrer-Informationen bei ausgehenden Requests:
Referrer-Policy: no-referrer
Cache-Control: no-store
no-referrer reduziert einen Leakage-Kanal; es löscht weder ein Postfach noch Browser-Verlauf, Proxy-Datensätze oder Anwendungslogs. no-store ist eine vernünftige Cache-Haltung für eine Antwort mit Credential, doch auch Cache-Verhalten ist nur eine Schicht.
Vor allem muss das Token erneut validiert werden, wenn das Formular mit dem neuen Passwort abgesendet wird. PortSwigger dokumentiert Abläufe, die ein Token auf der ersten Seite prüfen, den abschließenden Write aber nicht daran binden. Ein gültig aussehendes Formular ist keine Authorization. Der POST-Handler muss unabhängig feststellen, dass das eingereichte Token authentisch, nicht abgelaufen, unbenutzt und mit dem Konto verknüpft ist, dessen Passwort geändert wird. Das Formular benötigt außerdem die normale CSRF-Abwehr der Anwendung; der Besitz einer Reset-URL und der Schutz vor Cross-Site Submission beantworten unterschiedliche Fragen.
Einmalig konsumieren, auch bei Concurrency
„Single use“ ist Datenbankverhalten, kein Etikett. Zwei Requests können nahezu gleichzeitig eintreffen. Wenn beide used_at IS NULL lesen, bevor einer den Wert schreibt, können beide eine naive Prüfung bestehen. Die Consumption muss eine Transaction mit geeignetem Locking oder ein Conditional Update verwenden, das nur erfolgreich ist, solange das Token aktiv bleibt. Passwortersetzung und Token-Invalidierung sollten eine zusammenhängende State-Änderung bilden, die bei einem Fehler zurückgerollt wird.
Das genaue SQL hängt von Datenbank und Schema ab, weshalb ein allgemeiner Snippet wichtige Annahmen verbergen würde. Die Invariante ist testbar: Bei zwei gleichzeitigen Submissions desselben gültigen Tokens darf höchstens eine die Passwortänderung abschließen. Abgelaufene, bereits verwendete, fehlerhafte und unbekannte Tokens sollten gleichermaßen scheitern, ohne zu verraten, welcher interne Zustand vorlag.
Das neue Passwort sollte über denselben geprüften Pfad gehasht werden, der bei Registrierung und normalen Passwortänderungen verwendet wird. Die PHP-Dokumentation zu password_hash() empfiehlt ausreichend Platz für wachsende Algorithmusausgaben, da sich PASSWORD_DEFAULT ändern kann. Bei Richtlinien betont die aktuelle Anleitung in NIST SP 800-63B Länge, das Blockieren verbreiteter oder kompromittierter Werte, die Zulassung von Password Managern und Paste sowie den Verzicht auf willkürliche Composition Rules oder routinemäßig erzwungene Wechsel. NIST-Anforderungen gelten für den dort definierten Assurance-Kontext. Eine Anwendung sollte sie deshalb auf ihr eigenes Threat Model abbilden, statt eine einzelne Regel isoliert zu zitieren.
Den Vorgang schließen, statt eine verborgene Session zu öffnen
Nach einer erfolgreichen Änderung empfiehlt OWASP eine Benachrichtigung darüber, dass das Passwort zurückgesetzt wurde, ohne das Passwort einzuschließen. Die Nachricht gibt dem Kontoinhaber Gelegenheit zu reagieren, falls der Vorgang unerwartet war. Sie sollte auf einen normalen Support- oder Security-Weg verweisen und nicht das verbrauchte Credential wiederverwenden.
Bestehende Sessions erfordern eine ausdrückliche Entscheidung. Automatische Revocation begrenzt den Nutzen einer Session, die ein Angreifer möglicherweise bereits besitzt; eine Auswahl für den Benutzer kann die Störung bei legitimen Resets reduzieren. Der sicherere Default hängt vom Anwendungsrisiko und davon ab, ob serverseitige Revocation tatsächlich möglich ist. Das gewählte Verhalten muss dokumentiert und getestet werden. Das Löschen eines Browser-Cookies ist keine globale Revocation, solange andere Session-Datensätze gültig bleiben.
Nur weil sich das Passwort geändert hat, sollte nicht automatisch eine authentifizierte Session erzeugt werden. Wenn der Benutzer den normalen Login-Pfad durchläuft, bleiben Session-Erstellung, MFA, Risikoprüfungen und Audit-Verhalten an einer Stelle. Recovery sollte nur die minimal erforderliche Capability gewähren: die Wahl eines Ersatzpassworts, nicht allgemeinen Kontozugriff.
Invarianten statt nur den Happy Path testen
Eine Review-Checkliste für eine kleine Anwendung kann knapp bleiben:
- Existierende und nicht existierende Konten erzeugen denselben öffentlichen Status, dieselbe Nachricht und dieselbe Seitenform, ohne offensichtliche Timing-Trennung.
- Rate Controls decken sowohl Floods nach Quelle als auch wiederholte Requests gegen ein einzelnes Konto ab.
- Tokens stammen aus einem CSPRNG, sind opak, werden als Digests gespeichert, laufen ab und können nicht wiederverwendet werden.
- Reset-URLs verwenden einen konfigurierten HTTPS-Origin statt Host-Daten aus dem Request.
- Die Reset-Seite vermeidet Drittanbieter-Requests und sendet restriktive Referrer- und Cache-Header.
- Der abschließende POST validiert das Token erneut und wendet CSRF-Schutz an.
- Gleichzeitige Nutzung erlaubt höchstens eine erfolgreiche Passwortersetzung.
- Das neue Passwort folgt der normalen Richtlinie und dem normalen Hashing-Pfad.
- Die Session-Behandlung entspricht der dokumentierten Revoke-or-Retain-Richtlinie.
- Der Benutzer erhält eine Benachrichtigung, während Secrets und Passwörter aus Logs und E-Mail-Inhalten fernbleiben.
Diese Tests beweisen weder, dass ein Postfach sicher ist, noch dass die Anwendung keinen Authentifizierungsfehler besitzt. Sie machen den Vertrag jedoch beobachtbar. Das ist besonders in einer kleinen Codebase wertvoll, in der Recovery-Logik über Controller, Queue Worker, E-Mail-Template, Datenbanktabelle und Session Store verteilt sein kann.
Fazit
Ein Password-Reset-Link ist ein temporäres Credential mit einer sehr eng begrenzten Aufgabe. Ein sorgfältiger Ablauf verbirgt die Existenz eines Kontos, widersteht Request Flooding, stellt aus vertrauenswürdiger Konfiguration ein opakes, ablaufendes Token aus, begrenzt Token Leakage, validiert bei der Submission erneut, konsumiert atomar und schließt mit bewusstem Verhalten für Passwort, Session und Benachrichtigung ab.
Keine einzelne PHP-Zeile liefert all diese Eigenschaften. Das zufällige Token ist wichtig, aber ebenso wichtig sind die Übergänge darum herum und die Belege aus Failure-Path-Tests. Das ehrliche Ziel ist kein „vollkommen sicheres Reset-Formular“. Es ist ein Recovery-Pfad mit expliziten Annahmen, minimaler Authority und Fehlern, die sich nur schwer in Kontozugriff verwandeln lassen.
References
- OWASP Cheat Sheet Series: Forgot Password Cheat Sheet
- NIST Special Publication 800-63B: Digital Identity Guidelines, Authentication and Authenticator Management
- PHP Manual: random_bytes
- PHP Manual: password_hash
- MDN Web Docs: Referrer-Policy header
- PortSwigger Web Security Academy: Vulnerabilities in Other Authentication Mechanisms
