
header('Location: /ziel') gefolgt von exit, immer beides. Ohne expliziten Statuscode sendet PHP eine 302, nimm 301 für einen endgültigen Umzug und 303 nach der Verarbeitung eines Formulars. Ein Ziel, das vom Nutzer kommt, muss validiert werden, sonst öffnest du eine Weiterleitung auf eine fremde Website.
Eine Weiterleitung in PHP braucht zwei Zeilen. Einen Artikel wert ist der Rest: die Wahl des HTTP-Statuscodes, der Abbruch des Skripts dahinter und die Open-Redirect-Lücke, die jede Weiterleitung aufreißt, deren Ziel aus einer Nutzereingabe stammt.
Die minimale Weiterleitung
<?php
header('Location: /nouvelle-page');
exit;Zwei Punkte werden häufig missverstanden. Erstens akzeptiert Location eine absolute URL genauso wie einen Pfad relativ zur Wurzel: Beides funktioniert in allen aktuellen Browsern. Zweitens sendet PHP ohne expliziten Statuscode ein 302 Found.
Den richtigen Statuscode wählen
| Code | Bedeutung | Methode bleibt erhalten | Wann du ihn nimmst |
|---|---|---|---|
| 301 | Dauerhaft verschoben | nein (POST wird GET) | Eine URL hat sich endgültig geändert |
| 302 | Gefunden, temporär | nein (POST wird GET) | Historischer Standard, besser nicht bewusst setzen |
| 303 | See Other | nein, nie | Nach der Verarbeitung eines Formulars |
| 307 | Temporär | ja | Wartung, vorübergehende Umschaltung |
| 308 | Permanent | ja | URL geändert, POST bleibt erhalten |
// Eine Seite ist dauerhaft umgezogen
header('Location: https://exemple.com/nouvelle-adresse', true, 301);
exit;
// Nach der Verarbeitung eines Formulars: verhindert das erneute Senden des POST beim Reload
header('Location: /merci', true, 303);
exit;Der 303 nach einem Formular hat einen Namen: Post/Redirect/Get. Ohne ihn verlangt ein Reload der Seite vom Browser, das Formular erneut zu senden, und die Bestellung geht zweimal raus.
Der 301 landet im Browser-Cache, manchmal für sehr lange. Eine versehentlich gesetzte permanente Weiterleitung wirst du nur mühsam wieder los: Besucher, die sie einmal erhalten haben, werden weiter umgeleitet, ohne den Server überhaupt zu fragen. Im Zweifel fängst du mit einem 302 oder einem 307 an.
Das Skript immer beenden
header() setzt einen Header, mehr nicht, abgebrochen wird nichts. Der Code dahinter läuft weiter, und seine Ausgabe geht im Rumpf der Antwort raus.
<?php
header('Location: /connexion', true, 301);
echo "CONTENU QUI FUIT"; // geht trotzdem rausGemessen mit Apache und PHP 8.5.10:
avec exit -> corps de la réponse : 0 octet
sans exit -> corps de la réponse : 16 octets, « CONTENU QUI FUIT »Der Browser folgt der Weiterleitung und zeigt diesen Rumpf nicht an, deshalb fällt das Problem nicht auf. Übertragen wird er trotzdem: curl, ein Proxy, ein Log dazwischen oder ein Crawler sehen ihn. Auf einer Seite, die nicht angemeldete Besucher wegleitet, geht damit genau der Inhalt über das Netz, den du schützen wolltest.
die() und exit() sind dasselbe
Eine Behauptung hält sich hartnäckig, auch in der vorherigen Fassung dieses Artikels: die() schließe die Verbindung, exit() halte sie offen. Das stimmt nicht. Es sind zwei Schreibweisen desselben Sprachkonstrukts, und die PHP-Dokumentation sagt das ausdrücklich.
Zur Kontrolle zwei Dateien, die sich in einem einzigen Wort unterscheiden, ausgeliefert vom selben Apache:
<?php header('HTTP/1.1 304 Not Modified'); exit(); // avec-exit.php
<?php header('HTTP/1.1 304 Not Modified'); die(); // avec-die.php$ curl -s -D - -o /dev/null http://localhost/avec-exit.php
HTTP/1.1 304 Not Modified
Server: Apache/2.4.68 (Debian)
X-Powered-By: PHP/8.5.10
$ curl -s -D - -o /dev/null http://localhost/avec-die.php
HTTP/1.1 304 Not Modified
Server: Apache/2.4.68 (Debian)
X-Powered-By: PHP/8.5.10Beide Antworten sind Byte für Byte identisch, vom Header Date abgesehen. Die Wahl zwischen beiden ist eine Stilfrage. exit ist in modernem Code verbreiteter, und ohne Klammern geschrieben funktioniert es genauso.
Die Falle der bereits gesendeten Header
Das ist der häufigste Fehler bei diesem Thema. Das kleinste Byte, das vor header() ausgegeben wird, ein Leerzeichen vor <?php, eine Leerzeile nach ?> in einer eingebundenen Datei, ein echo zum Debuggen, ein BOM am Dateianfang, macht die Weiterleitung unmöglich.
Warning: Cannot modify header information - headers already sent by
(output started at /var/www/inc/config.php:42) in /var/www/page.php on line 3Die Meldung nennt in Klammern die verantwortliche Datei und Zeile. Um vorher zu prüfen:
if (headers_sent($fichier, $ligne)) {
error_log("Sortie déjà commencée dans {$fichier}:{$ligne}");
// Fallback: Weiterleitung im Browser
echo '<meta http-equiv="refresh" content="0;url=/nouvelle-page">';
exit;
}
header('Location: /nouvelle-page', true, 303);
exit;Zwei Regeln verhindern das Problem an der Wurzel: eine PHP-Datei nie mit ?> abschließen und nichts ausgeben, bevor die Antwort feststeht.
Der Open Redirect
Das ist die eigentliche Sicherheitslücke bei diesem Thema, und in den meisten Tutorials fehlt sie. Das Muster „nach dem Login zurück auf die vorherige Seite“ wird oft so geschrieben:
// Angreifbar
header('Location: ' . $_GET['retour']);
exit;Ein Angreifer verschickt daraufhin einen Link der Form https://votre-site.com/connexion.php?retour=https://site-pirate.example. Der Link trägt deine eigene Domain, kommt an den Mailfiltern vorbei und beruhigt das Opfer, das auf einer Kopie deiner Login-Seite landet. Das ist der Grundmechanismus eines großen Teils aller Phishing-Kampagnen.
Die Korrektur besteht nicht darin, Zeichen zu filtern, sondern darin, ausschließlich interne Ziele zu akzeptieren.
<?php
declare(strict_types=1);
/**
* Lässt nur einen internen Pfad zu. Jede absolute URL wird verworfen.
*/
function destinationSure(string $demande, string $defaut = '/'): string
{
// Steuerzeichen: Versuch einer Header-Injection
if ($demande === '' || preg_match('/[\x00-\x1F\x7F]/', $demande) === 1) {
return $defaut;
}
// Genau ein „/“ am Anfang, weder von „/“ noch von „\“ gefolgt
if (!preg_match('#^/(?![/\\\\])#', $demande)) {
return $defaut;
}
$parties = parse_url($demande);
if ($parties === false || isset($parties['scheme']) || isset($parties['host'])) {
return $defaut;
}
// Kein Path Traversal
if (str_contains($parties['path'] ?? '', '../')) {
return $defaut;
}
return $demande;
}
function redirigerVers(string $demande, string $defaut = '/'): never
{
header('Location: ' . destinationSure($demande, $defaut), true, 303);
exit;
}Unsere erste Fassung dieser Funktion prüfte nur den doppelten Slash am Anfang. Die Tests haben gezeigt, dass sie drei Familien von Eingaben durchließ, jede der ergänzten Zeilen hat also ihren Grund:
/site-pirate.example, Browser behandeln den Backslash an dieser Stelle wie einen Slash. Die verbreitetste Umgehung einer Prüfung, die nur auf//schaut./okrnX-Injecte: 1—header()würde das ablehnen, aber der Wert fliegt besser schon vorher raus./ok/../../admin, ein Path Traversal, der in einen nicht vorgesehenen Bereich führt.
Die fünfzehn Eingaben, die durch die finale Fassung gelaufen sind:
| Übergebener Wert | Ziel |
|---|---|
/mon-compte | /mon-compte |
/articles?page=2 | /articles?page=2 |
/a/b?x=1#ancre | /a/b?x=1#ancre |
https://site-pirate.example | / |
//site-pirate.example | / |
/site-pirate.example | / |
javascript:alert(1) | / |
/../admin und /ok/../../admin | / |
/okrnX-Injecte: 1 | / |
mon-compte (ohne Slash) und leerer String | / |
Sind externe Ziele wirklich nötig, ist die einzige sichere Form eine Whitelist von Domains, verglichen mit dem von parse_url() extrahierten Host, niemals mit str_contains().
Die Header-Injection
Ein Zeilenumbruch in einem Header-Wert erlaubte es früher, weitere Header anzuhängen. PHP filtert Zeilenumbrüche in header() seit Version 5.1.2 und wirft einen Fehler. Mehr ist dazu nicht zu tun, aber es ist ein weiterer Grund, Header nicht durch Verkettung von Nutzerdaten zu bauen.
Wann du nicht in PHP weiterleiten solltest
Eine permanente Weiterleitung von URL zu URL braucht kein PHP. Auf Server-Ebene ist sie schneller, die Anfrage weckt den Interpreter gar nicht erst, und sie überlebt einen Ausfall der Anwendung.
Redirect 301 /ancienne-page /nouvelle-page
# HTTPS erzwingen
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]location = /ancienne-page {
return 301 /nouvelle-page;
}PHP bleibt die richtige Wahl, sobald das Ziel von einem Zustand abhängt: Session, Berechtigungen, Ergebnis einer Verarbeitung, ausgehandelte Sprache.
Das Wichtigste in Kürze
header('Location: …')und danachexit, immer beides.- 303 nach einem Formular, 301 nur wenn es endgültig ist.
die()undexit()sind identisch, gemessen und geprüft.- Ein Ziel aus Nutzereingaben wird gegen interne Pfade validiert, absolute URLs und der doppelte Slash fliegen raus.
Der 303 nach einem Formular geht oft mit einer Empfangsbestätigung einher: siehe eine E-Mail mit PHP versenden. Wenn das Ziel im Browser statt auf dem Server berechnet werden soll, siehe Variablen von PHP an JavaScript übergeben. Zur Weiterleitung nach einem Datei-Upload siehe eine Datei in PHP hochladen.
Siehe auch den Hub Webentwicklung und die Verbindung zu einer Datenbank in PHP.
Häufige Fehler
Date abgesehen.header() bricht nichts ab. Der Inhalt dahinter geht im Antwortrumpf raus: im Browser unsichtbar, für curl, einen Proxy oder einen Crawler aber sehr wohl da.header('Location: ' . $_GET['retour']) erlaubt es, einen Phishing-Link mit deiner eigenen Domain zu bauen. Das Ziel muss als interner Pfad validiert werden.//site-pirate.example ist eine protokollrelative URL. Und /site-pirate.example kommt ebenfalls durch: Browser behandeln den Backslash an dieser Stelle wie einen Slash.<?php, eine Leerzeile nach ?> oder ein BOM genügen. Eine PHP-Datei nie mit ?> abschließen.

