
header('Location: /bestemming') gevolgd door exit, altijd allebei. Zonder expliciete statuscode stuurt PHP een 302, gebruik 301 voor een definitieve verhuizing en 303 na het verwerken van een formulier. Een bestemming die de gebruiker aanlevert, moet je valideren, anders open je een redirect naar een externe site.
Redirecten in PHP is een kwestie van twee regels. Wat wél een artikel waard is: de keuze van de statuscode, het stoppen van het script erachter, en de open redirect die je opent zodra de bestemming uit gebruikersinvoer komt.
De minimale redirect
<?php
header('Location: /nouvelle-page');
exit;Twee punten worden vaak verkeerd begrepen. Ten eerste accepteert Location zowel een absolute URL als een pad vanaf de root: allebei werken ze in elke huidige browser. Ten tweede stuurt PHP zonder expliciete statuscode een 302 Found.
De juiste statuscode kiezen
| Code | Betekenis | Methode blijft behouden | Wanneer gebruiken |
|---|---|---|---|
| 301 | Permanent verplaatst | nee (POST wordt GET) | Een URL is definitief veranderd |
| 302 | Found, tijdelijk | nee (POST wordt GET) | Historische standaard, schrijf hem liever niet |
| 303 | See Other | nee, nooit | Na een verwerkt formulier |
| 307 | Tijdelijk | ja | Onderhoud, tijdelijke omschakeling |
| 308 | Permanent | ja | URL gewijzigd, met behoud van POST |
// Een pagina is definitief verhuisd
header('Location: https://exemple.com/nouvelle-adresse', true, 301);
exit;
// Na de verwerking van een formulier: voorkomt dat de POST bij een reload opnieuw wordt verstuurd
header('Location: /merci', true, 303);
exit;De 303 na een formulier heeft een naam: Post/Redirect/Get. Zonder die stap vraagt een refresh van de pagina de browser om het formulier opnieuw te versturen, en gaat de bestelling twee keer de deur uit.
Een 301 blijft in de cache van de browser hangen, soms heel lang. Een permanente redirect die je per ongeluk hebt gezet, krijg je moeilijk weer weg: bezoekers die hem ooit hebben ontvangen worden doorgestuurd zonder de server nog te bevragen. Bij twijfel begin je met een 302 of een 307.
Stop het script, altijd
header() zet een header klaar, meer niet. De code erna draait gewoon door en wat die uitvoert, belandt in de body van de respons.
<?php
header('Location: /connexion', true, 301);
echo "CONTENU QUI FUIT"; // toch verstuurdGemeten met Apache en 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 »De browser volgt de redirect en toont die body niet, waardoor het probleem onzichtbaar blijft. Verstuurd wordt hij wel: curl, een proxy, een log ertussenin of een crawler zien hem gewoon. Op een pagina die een niet-ingelogde bezoeker wegstuurt, gaat precies de inhoud het netwerk op die je wilde afschermen.
die() en exit() zijn hetzelfde
Er doet een bewering de ronde, ook in de vorige versie van dit artikel: die() zou de verbinding sluiten en exit() niet. Dat klopt niet. Het zijn twee schrijfwijzen van dezelfde taalconstructie, en de PHP-documentatie zegt dat met zoveel woorden.
De controle: twee bestanden die op één woord na identiek zijn, geserveerd door dezelfde 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.10De twee responses zijn byte voor byte identiek, op de header Date na. De keuze tussen beide is een kwestie van stijl. exit kom je in moderne code vaker tegen, en zonder haakjes schrijven werkt ook.
De valkuil van al verstuurde headers
Dit is de meest voorkomende fout op dit onderwerp. De kleinste byte die vóór header() wordt geschreven, een spatie vóór <?php, een lege regel na ?> in een include, een echo om te debuggen, een BOM aan het begin van het bestand, maakt de redirect onmogelijk.
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 3De melding noemt tussen haakjes het schuldige bestand en de regel. Om te testen voor je iets doet:
if (headers_sent($fichier, $ligne)) {
error_log("Sortie déjà commencée dans {$fichier}:{$ligne}");
// terugval: redirect aan de browserkant
echo '<meta http-equiv="refresh" content="0;url=/nouvelle-page">';
exit;
}
header('Location: /nouvelle-page', true, 303);
exit;Twee regels houden het probleem bij de bron weg: sluit een PHP-bestand nooit af met ?>, en zet niets op het scherm voor je hebt besloten wat de respons wordt.
De open redirect
Dit is het echte beveiligingslek van dit onderwerp, en het ontbreekt in de meeste tutorials. Het patroon “na het inloggen terug naar de vorige pagina” wordt vaak zo geschreven:
// Kwetsbaar
header('Location: ' . $_GET['retour']);
exit;Een aanvaller stuurt dan een link in de vorm https://votre-site.com/connexion.php?retour=https://site-pirate.example. De link draagt jouw domeinnaam, komt door de mailfilters heen en stelt het slachtoffer gerust, dat vervolgens op een kopie van jouw inlogpagina belandt. Zo werkt een flink deel van de phishingcampagnes.
De oplossing is niet tekens filteren, maar alleen interne bestemmingen accepteren.
<?php
declare(strict_types=1);
/**
* Staat alleen een intern pad toe. Elke absolute URL wordt geweigerd.
*/
function destinationSure(string $demande, string $defaut = '/'): string
{
// Stuurtekens: poging tot header injection
if ($demande === '' || preg_match('/[\x00-\x1F\x7F]/', $demande) === 1) {
return $defaut;
}
// Eén enkele “/” aan het begin, niet gevolgd door een “/” of een backslash
if (!preg_match('#^/(?![/\\\\])#', $demande)) {
return $defaut;
}
$parties = parse_url($demande);
if ($parties === false || isset($parties['scheme']) || isset($parties['host'])) {
return $defaut;
}
// Geen 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;
}Onze eerste versie van deze functie controleerde alleen de dubbele slash aan het begin. Uit de tests bleek dat er drie soorten invoer doorheen kwamen, elke toegevoegde regel is daarop terug te voeren:
/site-pirate.example, browsers behandelen de backslash op deze positie als een slash. De meest gebruikte omweg rond een controle die alleen naar//kijkt./okrnX-Injecte: 1—header()zou dit weigeren, maar je zet de waarde beter opzij voor het zover komt./ok/../../admin, path traversal die in een niet voorziene zone uitkomt.
De vijftien waarden die door de definitieve versie zijn gehaald:
| Ontvangen waarde | Bestemming |
|---|---|
/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 en /ok/../../admin | / |
/okrnX-Injecte: 1 | / |
mon-compte (zonder slash) en de lege string | / |
Zijn externe bestemmingen echt nodig, dan is de enige veilige vorm een whitelist van domeinen, vergeleken op de host die parse_url() eruit haalt, nooit met str_contains().
Header injection
Een regeleinde in een headerwaarde maakte het vroeger mogelijk om er andere headers achteraan te plakken. PHP filtert regeleindes in header() sinds versie 5.1.2 en gooit een foutmelding. Verder hoef je niets te doen, maar het is een reden te meer om headers niet op te bouwen door gebruikersdata aan elkaar te plakken.
Wanneer je niet in PHP moet redirecten
Een permanente redirect van URL naar URL heeft geen PHP nodig. Op serverniveau is het sneller, de request wekt de interpreter niet, en het blijft werken als de applicatie eruit ligt.
Redirect 301 /ancienne-page /nouvelle-page
# HTTPS afdwingen
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]location = /ancienne-page {
return 301 /nouvelle-page;
}PHP blijft op zijn plaats zodra de bestemming van een toestand afhangt: sessie, rechten, uitkomst van een verwerking, onderhandelde taal.
Om te onthouden
header('Location: …')en danexit, altijd allebei.- 303 na een formulier, 301 alleen als het definitief is.
die()enexit()zijn identiek, gemeten en geverifieerd.- Een bestemming die van de gebruiker komt, valideer je tegen interne paden: geen absolute URL’s, geen dubbele slash.
De 303 na een formulier gaat vaak samen met een bevestiging: zie een e-mail versturen met PHP. Moet de bestemming in de browser worden bepaald in plaats van op de server, zie variabelen doorgeven van PHP aan JavaScript. Voor de redirect na een bestandsupload, zie een bestand uploaden in PHP.
Zie ook de hub Webontwikkeling en verbinding maken met een database in PHP.
Veelgemaakte fouten
Date na.header() onderbreekt niets. Wat erna komt gaat mee in de body van de respons: onzichtbaar in de browser, maar wel degelijk aanwezig voor curl, een proxy of een crawler.header('Location: ' . $_GET['retour']) bouwt een aanvaller een phishinglink die jouw domeinnaam draagt. De bestemming moet als intern pad worden gevalideerd.//site-pirate.example is een protocolrelatieve URL. En /site-pirate.example komt er ook doorheen: browsers behandelen de backslash op die positie als een slash.<?php, een lege regel na ?> of een BOM is al genoeg. Sluit een PHP-bestand nooit af met ?>.

