Open Redirects in kleinen PHP-Anwendungen - Lassen Sie den Server das Ziel wählen
Ein Anmeldeformular muss jemanden oft zu der Seite zurückbringen, die ursprünglich aufgerufen werden sollte. Ein Checkout kann mit einem Beleg fortfahren, eine administrative Aktion zu einer Liste zurückkehren. Ein Parameter wie next oder return_to scheint eine bequeme Möglichkeit zu sein, diesen Ablauf zu bewahren. Die Sicherheitsfrage lautet nicht, ob die Anwendung weiterleiten darf. Sie lautet: Wer darf das Ziel wählen?
Wenn Request-Daten ein beliebiges Ziel vorgeben können, kann aus einer nützlichen Navigationsfunktion ein Open Redirect werden. Die erste URL gehört weiterhin zur legitimen Anwendung, doch ihre Antwort kann den Browser an einen Ort senden, den die Anwendung nie vorgesehen hat. Das sicherere Design ist meist kein klügerer Versuch, jede schädliche URL zu erkennen. Stattdessen sollte nicht vertrauenswürdige Eingabe weniger Befugnis erhalten.
Was macht einen Redirect offen?
CWE-601 beschreibt die grundlegende Schwäche als eine Anwendung, die benutzergesteuerte Eingaben zur Angabe eines externen Links akzeptiert und diesen Link für eine Weiterleitung verwendet. Das OWASP Cheat Sheet formuliert es ähnlich: Nicht vertrauenswürdige Eingabe erreicht ein Ziel, ohne dass ausreichend entschieden wurde, ob dieses Ziel gültig und für die Anwendung angemessen ist.
In PHP ist die kürzeste verwundbare Form leicht zu erkennen:
<?php
$next = $_GET['next'] ?? '/';
header('Location: ' . $next);
exit;
Das Problem ist nicht header() selbst. Das Problem ist der ununterbrochene Pfad von einem Request-Parameter zum Feld Location. Ein Query-Parameter ist die offensichtliche Quelle, doch dieselbe Vertrauensfrage gilt für Formularfelder, Cookies, Request-Header, ursprünglich von einem Benutzer gelieferte Datenbankwerte oder Antworten anderer Dienste. Das Speichern eines Wertes macht ihn nicht vertrauenswürdig.
Der Kontext bleibt wichtig. Ein Dienst, der absichtlich auf beliebige Links weiterleitet, hat andere Anforderungen als der Login-Ablauf eines CMS. CWE weist ausdrücklich darauf hin, dass das beabsichtigte Verhalten beeinflusst, ob eine Schwachstelle vorliegt. Bei einem gewöhnlichen Rückkehr-Ablauf gewährt beliebige Navigation über Origin-Grenzen hinweg jedoch meist mehr Befugnis, als die Funktion benötigt.
Der Schaden beruht oft auf geliehenem Vertrauen
Ein Angreifer kann eine legitime Domain an den Anfang eines Links setzen und zugleich dafür sorgen, dass das endgültige Ziel eine nicht vertrauenswürdige Seite ist. OWASP und CWE nennen Phishing als mögliche Folge: Die vertraute erste Domain kann den Link glaubwürdiger erscheinen lassen, obwohl der Browser sie nach der Antwort verlässt. Das bedeutet nicht, dass jeder Open Redirect den gleichen Schweregrad hat oder Zugangsdaten stehlen wird. Die Auswirkung hängt davon ab, wo die Weiterleitung erscheint, was Benutzer erwarten und ob ein anderer Workflow vom Ziel abhängt.
Ein Redirect darf außerdem niemals Authorization ersetzen. Einen nicht berechtigten Benutzer von einer Admin-Route wegzuleiten reicht nicht aus, wenn die Route ihre Aktion vor der Weiterleitung ausführt oder ein anderer Request denselben Handler direkt erreichen kann. Der Ziel-Endpoint muss seine eigenen Zugriffsregeln durchsetzen. Navigation bestimmt, wohin der Browser geht; Authorization bestimmt, was der Server erlaubt.
Zielnamen sind Ziel-URLs vorzuziehen
Wenn ein Ablauf drei gültige Ergebnisse hat, sollte er drei Identifier statt des gesamten URL-Raums akzeptieren. OWASP und CWE empfehlen beide, einen kurzen Namen, eine ID oder ein Token serverseitig auf ein kontrolliertes Ziel abzubilden. Der Request darf zwischen genehmigten Optionen wählen, aber keine vierte Option erfinden.
Angenommen, ein erfolgreicher POST darf entweder zu einem Dashboard oder zur Seite für Kontosicherheit führen:
<?php
$destinations = [
'dashboard' => '/dashboard',
'security' => '/account/security',
];
$key = $_POST['next'] ?? 'dashboard';
$target = $destinations[$key] ?? $destinations['dashboard'];
header('Location: ' . $target, true, 303);
exit;
Das Beispiel ist absichtlich eng begrenzt. Authentication, CSRF-Schutz und die übermittelte Aktion benötigen weiterhin eigene Kontrollen; sie wurden hier ausgelassen, damit die Redirect-Entscheidung sichtbar bleibt. Ein unbekannter Key fällt auf einen bekannten Pfad zurück. Der Client liefert niemals die Bytes, die in Location eingesetzt werden.
Dieses Muster bleibt auch bei Änderungen an Routes stabil. Der öffentliche Identifier kann security bleiben, während sich der serverseitige Pfad ändert. In einer größeren Anwendung können benannte Routes dieselbe Trennung ermöglichen, ohne in jedem Controller eine zweite Map zu pflegen.
Wenn eine externe Übergabe wirklich erforderlich ist
Manche Anwendungen müssen ihren eigenen Origin verlassen: Dokumentation, ein Zahlungsanbieter oder ein anderer Dienst kann ein legitimes Ziel sein. Bei einer kleinen Auswahl funktioniert weiterhin eine feste Zuordnung:
<?php
$externalDestinations = [
'documentation' => 'https://docs.example.org/start',
'status' => 'https://status.example.org/',
];
$key = $_GET['to'] ?? '';
if (!array_key_exists($key, $externalDestinations)) {
http_response_code(400);
exit('Unknown destination');
}
header('Location: ' . $externalDestinations[$key], true, 302);
exit;
Wenn Ziele konfigurierbar sein müssen, statt im Code festzustehen, verlagert sich die Vertrauensentscheidung in diesen Konfigurations-Workflow. Es sollte eingeschränkt werden, wer ein Ziel hinzufügen darf; Änderungen sollten geprüft und geparste, kanonisierte Werte gespeichert werden, statt jede eingereichte Zeichenfolge als genehmigt zu behandeln.
Für eine größere, aber weiterhin kontrollierte Menge empfiehlt OWASP das Parsen mit einem gepflegten Parser, der mit der Komponente kompatibel ist, die die URL verwenden wird. Danach werden Scheme, kanonischer Host, effektiver Port und erforderliche Pfadbeschränkungen mit einer expliziten Allowlist verglichen. Das ist aufwendiger als die Prüfung einer Hostname-Zeichenfolge. Außerdem sind Tests für mehrdeutige Eingaben und Parser-Unterschiede erforderlich. Eine Bestätigungsseite, die das externe Ziel deutlich anzeigt, kann eine beabsichtigte Übergabe sichtbar machen. Sie ist jedoch eine zusätzliche Maßnahme für Bedienbarkeit und Defense, keine Erlaubnis, die Validierung zu überspringen.
Warum vertraute String-Prüfungen fragil sind
Eine Prüfung wie str_starts_with($url, 'https://example.com') stellt eine Frage über eine Zeichenfolge, nicht über die Authority. Ein längerer Hostname, eine User-Information-Komponente, ungewöhnliche Slashes, Backslashes, Encoding oder unterschiedliche Parser können verändern, welches Ziel ein Browser erkennt. OWASP warnt deshalb vor rohen Prefix- und Suffix-Vergleichen.
Auch parse_url() verwandelt Eingaben nicht in eine vertrauenswürdige URL. Das PHP-Handbuch erklärt, dass die Funktion eine URL in Bestandteile zerlegt und nicht zu ihrer Validierung gedacht ist; auch partielle und ungültige URLs können akzeptiert werden. Es warnt außerdem davor, dass die Validierung mit einem Parser und die spätere Verwendung mit einem anderen Parser ein Sicherheitsproblem erzeugen kann. Der WHATWG URL Standard soll unter anderem interoperables URL-Parsing in Browsern ermöglichen. Sein Sicherheitsabschnitt warnt, dass zwischen Parteien übergebene URLs aus nicht vertrauenswürdigen Quellen stammen können.
Parsing bleibt nützlich, wenn externe URLs unvermeidbar sind. Es macht Komponenten sichtbar, die mit einer Policy verglichen werden können. Doch „der Parser hat ein Array geliefert“ ist lediglich eine syntaktische Beobachtung. Die Anwendung muss weiterhin entscheiden, welche Schemes, Hosts, Ports, Pfade und Credentials akzeptabel sind. Auf PHP-Versionen mit standardkonformen URI-Klassen können diese Klassen Parser-Mehrdeutigkeit verringern, aber sie erfinden nicht die Allowlist der Anwendung.
HTTP-Mechanik und Vertrauensentscheidung trennen
Nach RFC 9110 enthält Location eine URI-Referenz. Der Statuscode beschreibt, wie die Antwort zur nächsten Anfrage in Beziehung steht. Die PHP-Dokumentation zu header() erklärt, dass ein Feld Location standardmäßig den Status 302 erzeugt, sofern nicht bereits 201 oder ein anderer 3xx-Status gesetzt wurde.
Es ist klarer, den Status bewusst zu wählen. Im POST-Beispiel oben bedeutet 303, dass die ausgewählte Ressource mit GET oder HEAD abgerufen werden kann, was zu einer Ansicht nach einer Aktion passt. Eine temporäre Weiterleitung von einem GET-Endpoint kann 302 verwenden. Die Statuswahl repariert kein nicht vertrauenswürdiges Ziel: Ein sorgfältig gewählter 303 zu einer vom Angreifer kontrollierten URL bleibt ein Open Redirect.
PHP muss Header vor der Response-Ausgabe senden. Nach dem Planen der Weiterleitung läuft die Ausführung außerdem weiter, sofern das Programm nicht beendet wird oder über ein Framework-Response zurückkehrt. Ein unmittelbares exit nach einer einfachen Weiterleitung mit header() verhindert, dass nachfolgender Code versehentlich ausgeführt wird. Das ist Control-Flow-Hygiene und von der Zielvalidierung getrennt.
Den gesamten Redirect-Pfad prüfen
Eine sinnvolle Prüfung beginnt bei den Redirect-Sinks und verfolgt den Datenfluss rückwärts. Gesucht werden sollten Location:, Redirect-Helper des Frameworks und clientseitige Navigation wie location.assign(). Clientseitiger Code kann denselben Fehler an der Vertrauensgrenze wiederholen, obwohl Source und Sink dann im Browser statt in PHP liegen.
Für jeden Redirect sind folgende Fragen hilfreich:
- Kann ein Request-Feld, Cookie, gespeicherter Benutzerwert, Header oder eine externe Antwort das Ziel beeinflussen?
- Benötigt die Funktion wirklich eine URL, oder reicht ein benanntes Ziel?
- Falls eine externe URL nötig ist: Welche Kombinationen aus Scheme, Host, Port und Pfad sind ausdrücklich genehmigt?
- Verwenden Validierung und Navigation kompatible URL-Interpretationen?
- Führt ein ungültiger oder fehlender Wert geschlossen zu einer bekannten lokalen Seite oder einem Fehler?
- Setzt jedes Ziel Authorization unabhängig durch?
- Ist der Statuscode bewusst gewählt, und endet die Ausführung, nachdem die Antwort festgelegt wurde?
Tests sollten jeden genehmigten Identifier, einen unbekannten Identifier, einen leeren Wert, lokale und externe Formen, irreführende Hostnamen, User-Information-Syntax, mit // beginnende Network-Path-Referenzen, Backslashes, codierte Zeichen und überlange Eingaben umfassen. In Tests sollten reservierte Beispieldomains verwendet und ausschließlich Systeme geprüft werden, für deren Bewertung eine Berechtigung besteht. Das erwartete Ergebnis sollte eindeutig sein: Genehmigte Ziele leiten genau wie vorgesehen weiter; alles andere liefert einen Fehler oder einen bekannten Fallback.
Eine kleinere Entscheidung lässt sich leichter absichern
Redirect-Sicherheit wird schwierig, wenn die Anwendung fragt: „Ist diese beliebige Zeichenfolge überall sicher?“ Die Frage wird wesentlich handhabbarer, wenn sie lautet: „Ist dies eines der Ziele, die dieser Ablauf tatsächlich unterstützt?“ Eine serverseitige Map beantwortet diese kleinere Frage direkt.
Nicht jeder dynamische Redirect muss entfernt werden, und für manche Integrationen bleibt Parsing notwendig. Die wichtige Grenze besteht darin, dass nicht vertrauenswürdige Daten nicht stillschweigend zur Navigationsbefugnis werden. Zuerst sollten bekannte Ziele gewählt, komponentenbasierte Allowlists nur bei tatsächlichem Bedarf eingesetzt und Authorization am Ziel durchgesetzt werden. Dieses Design hängt nicht davon ab, jede raffinierte Schreibweise einer unerwünschten URL zu erkennen.
