
header('Location: /destination') suivi de exit, toujours les deux. Sans code explicite PHP envoie un 302, utilisez 301 pour un déplacement définitif et 303 après le traitement d'un formulaire. Une destination fournie par l'utilisateur doit être validée, sinon vous ouvrez une redirection vers un site tiers.
Rediriger en PHP tient en deux lignes. Ce qui mérite un article, c’est le choix du code HTTP, l’arrêt du script derrière, et la faille de redirection ouverte que produit toute redirection construite à partir d’une donnée envoyée par l’utilisateur.
La redirection minimale
<?php
header('Location: /nouvelle-page');
exit;Deux points souvent mal compris. D’abord, Location accepte une URL absolue comme un chemin relatif à la racine : les deux fonctionnent sur tous les navigateurs actuels. Ensuite, sans code explicite, PHP envoie un 302 Found.
Choisir le bon code
| Code | Sens | Méthode conservée | Quand l’utiliser |
|---|---|---|---|
| 301 | Déplacé définitivement | non (POST devient GET) | Une URL a changé pour de bon |
| 302 | Trouvé, temporaire | non (POST devient GET) | Défaut historique, à éviter d’écrire |
| 303 | Voir ailleurs | non, jamais | Après un formulaire traité |
| 307 | Temporaire | oui | Maintenance, bascule provisoire |
| 308 | Permanent | oui | URL changée, en préservant POST |
// Une page a déménagé définitivement
header('Location: https://exemple.com/nouvelle-adresse', true, 301);
exit;
// Après le traitement d'un formulaire : évite le renvoi du POST au rechargement
header('Location: /merci', true, 303);
exit;Le 303 après formulaire porte un nom, Post/Redirect/Get. Sans lui, un rafraîchissement de page redemande au navigateur de renvoyer le formulaire, et la commande part deux fois.
Le 301 se met en cache dans le navigateur, parfois pour très longtemps. Une redirection permanente posée par erreur est pénible à retirer : les visiteurs qui l’ont reçue continuent d’être redirigés sans même interroger le serveur. Dans le doute, commencer par un 302 ou un 307.
Toujours arrêter le script
header() place un en-tête, il n’interrompt rien. Le code qui suit s’exécute et son contenu part dans le corps de la réponse.
<?php
header('Location: /connexion', true, 301);
echo "CONTENU QUI FUIT"; // parti quand mêmeMesuré avec Apache et 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 »Le navigateur suit la redirection et n’affiche pas ce corps, ce qui rend le problème invisible. Mais il est bien transmis : curl, un proxy, un journal intermédiaire ou un robot le voient. Sur une page qui redirige un visiteur non authentifié, c’est exactement le contenu qu’on voulait protéger qui part sur le réseau.
die() et exit() sont la même chose
Une affirmation circule beaucoup, y compris dans la version précédente de cet article : die() fermerait la connexion et exit() la garderait ouverte. C’est faux. Ce sont deux graphies de la même construction de langage, et la documentation PHP le dit explicitement.
Vérification, deux fichiers identiques à un mot près, servis par le même 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.10Les deux réponses sont identiques octet pour octet, en-tête Date mis à part. Choisir entre les deux est une affaire de style. exit est plus courant dans le code moderne, et l’écrire sans parenthèses fonctionne aussi.
Le piège des en-têtes déjà envoyés
C’est l’erreur la plus fréquente sur ce sujet. Le moindre octet écrit avant header(), un espace avant <?php, une ligne vide après ?> dans un fichier inclus, un echo de débogage, un octet BOM en tête de fichier, rend la redirection impossible.
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 3Le message indique le fichier et la ligne coupables entre parenthèses. Pour tester avant d’agir :
if (headers_sent($fichier, $ligne)) {
error_log("Sortie déjà commencée dans {$fichier}:{$ligne}");
// repli : redirection côté navigateur
echo '<meta http-equiv="refresh" content="0;url=/nouvelle-page">';
exit;
}
header('Location: /nouvelle-page', true, 303);
exit;Deux règles évitent le problème à la source : ne jamais fermer un fichier PHP par ?>, et ne rien afficher avant d’avoir décidé de la réponse.
La redirection ouverte
C’est la vraie faille de sécurité de ce sujet, et elle est absente de la plupart des tutoriels. Le motif « revenir à la page précédente après connexion » se code souvent ainsi :
// Vulnérable
header('Location: ' . $_GET['retour']);
exit;Un attaquant envoie alors un lien de la forme https://votre-site.com/connexion.php?retour=https://site-pirate.example. Le lien porte votre nom de domaine, il passe les filtres de messagerie et rassure la victime, qui atterrit sur une copie de votre page de connexion. C’est le mécanisme de base d’une bonne partie des campagnes d’hameçonnage.
La correction ne consiste pas à filtrer les caractères, mais à n’accepter que des destinations internes.
<?php
declare(strict_types=1);
/**
* N'autorise qu'un chemin interne. Toute URL absolue est écartée.
*/
function destinationSure(string $demande, string $defaut = '/'): string
{
// Caractères de contrôle : tentative d'injection d'en-tête
if ($demande === '' || preg_match('/[\x00-\x1F\x7F]/', $demande) === 1) {
return $defaut;
}
// Un « / » unique en tête, non suivi d'un « / » ni d'un « \ »
if (!preg_match('#^/(?![/\\\\])#', $demande)) {
return $defaut;
}
$parties = parse_url($demande);
if ($parties === false || isset($parties['scheme']) || isset($parties['host'])) {
return $defaut;
}
// Pas de remontée de chemin
if (str_contains($parties['path'] ?? '', '../')) {
return $defaut;
}
return $demande;
}
function redirigerVers(string $demande, string $defaut = '/'): never
{
header('Location: ' . destinationSure($demande, $defaut), true, 303);
exit;
}Notre première version de cette fonction ne testait que le double slash de tête. Les essais ont montré qu’elle laissait passer trois familles d’entrées, ce qui justifie chacune des lignes ajoutées :
/site-pirate.example, les navigateurs traitent l’antislash comme un slash dans cette position. Le contournement le plus courant d’un contrôle qui ne regarde que//./okrnX-Injecte: 1—header()le refuserait, mais mieux vaut écarter la valeur avant d’arriver là./ok/../../admin, une remontée de chemin qui atteint une zone non prévue.
Les quinze entrées passées à la version finale :
| Valeur reçue | Destination |
|---|---|
/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 et /ok/../../admin | / |
/okrnX-Injecte: 1 | / |
mon-compte (sans slash) et chaîne vide | / |
Si des destinations externes sont réellement nécessaires, la seule forme sûre est une liste blanche de domaines, comparée sur l’hôte extrait par parse_url(), jamais par str_contains().
L’injection dans l’en-tête
Un retour à la ligne dans une valeur d’en-tête permettait autrefois d’en ajouter d’autres. PHP filtre les retours à la ligne dans header() depuis la version 5.1.2 et lève une erreur. Rien à faire de plus, mais c’est une raison supplémentaire de ne pas construire les en-têtes par concaténation de données utilisateur.
Quand ne pas rediriger en PHP
Une redirection permanente d’URL à URL n’a pas besoin de PHP. La faire au niveau du serveur est plus rapide, la requête ne réveille pas l’interpréteur, et survit à une panne de l’application.
Redirect 301 /ancienne-page /nouvelle-page
# Forcer HTTPS
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]location = /ancienne-page {
return 301 /nouvelle-page;
}PHP reste indiqué quand la destination dépend d’un état : session, droits, résultat d’un traitement, langue négociée.
À retenir
header('Location: …')puisexit, toujours les deux.- 303 après un formulaire, 301 seulement quand c’est définitif.
die()etexit()sont identiques, mesuré et vérifié.- Une destination venue de l’utilisateur se valide contre les chemins internes, en refusant les URL absolues et le double slash.
Le 303 après formulaire va souvent de pair avec un accusé de réception : voir envoyer un e-mail avec PHP. Si la destination doit être calculée côté navigateur plutôt que côté serveur, voir passer des variables de PHP à JavaScript. Pour la redirection qui suit un envoi de fichier, voir uploader un fichier en PHP.
Voir aussi le hub Développement web et la connexion à une base de données en PHP.
Erreurs fréquentes
Date mis à part.header() n'interrompt rien. Le contenu qui suit part dans le corps de la réponse : invisible dans le navigateur, bien présent pour curl, un proxy ou un robot.header('Location: ' . $_GET['retour']) permet de fabriquer un lien d'hameçonnage portant votre nom de domaine. La destination doit être validée comme chemin interne.//site-pirate.example est une URL relative au protocole. Et /site-pirate.example passe aussi : les navigateurs traitent l'antislash comme un slash à cette position.<?php, une ligne vide après ?> ou un octet BOM suffisent. Ne jamais fermer un fichier PHP par ?>.

