
header('Location: /destinazione') seguito da exit, sempre entrambi. Senza un codice esplicito PHP invia un 302, usa 301 per uno spostamento definitivo e 303 dopo l'elaborazione di un form. Una destinazione fornita dall'utente va validata, altrimenti apri un redirect verso un sito di terzi.
Fare un redirect in PHP sta in due righe. Quello che merita un articolo è la scelta del codice HTTP, l’arresto dello script subito dopo e la falla di open redirect che nasce da ogni redirect costruito su un dato inviato dall’utente.
Il redirect minimo
<?php
header('Location: /nouvelle-page');
exit;Due punti spesso fraintesi. Primo, Location accetta sia un URL assoluto sia un percorso relativo alla radice: entrambi funzionano su tutti i browser attuali. Secondo, senza un codice esplicito PHP invia un 302 Found.
Scegliere il codice giusto
| Codice | Significato | Metodo conservato | Quando usarlo |
|---|---|---|---|
| 301 | Spostato definitivamente | no (il POST diventa GET) | Un URL è cambiato per sempre |
| 302 | Trovato, temporaneo | no (il POST diventa GET) | Default storico, meglio non scriverlo |
| 303 | Vedi altrove | no, mai | Dopo l’elaborazione di un form |
| 307 | Temporaneo | sì | Manutenzione, deviazione provvisoria |
| 308 | Permanente | sì | URL cambiato, preservando il POST |
// Una pagina ha traslocato definitivamente
header('Location: https://exemple.com/nouvelle-adresse', true, 301);
exit;
// Dopo l'elaborazione di un form: evita il reinvio del POST al ricaricamento
header('Location: /merci', true, 303);
exit;Il 303 dopo un form ha un nome: Post/Redirect/Get. Senza, un aggiornamento della pagina chiede di nuovo al browser di reinviare il form, e l’ordine parte due volte.
Il 301 finisce nella cache del browser, a volte per moltissimo tempo. Un redirect permanente messo per sbaglio è fastidioso da togliere: i visitatori che l’hanno ricevuto continuano a essere reindirizzati senza nemmeno interrogare il server. Nel dubbio, parti da un 302 o da un 307.
Fermare sempre lo script
header() imposta un header, non interrompe niente. Il codice che segue viene eseguito e quello che stampa finisce nel corpo della risposta.
<?php
header('Location: /connexion', true, 301);
echo "CONTENU QUI FUIT"; // partito lo stessoMisurato con Apache e 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 »Il browser segue il redirect e non mostra quel corpo, il che rende il problema invisibile. Ma viene trasmesso lo stesso: curl, un proxy, un log intermedio o un bot lo vedono. Su una pagina che reindirizza un visitatore non autenticato, finisce in rete esattamente il contenuto che volevi proteggere.
die() ed exit() sono la stessa cosa
Gira parecchio un’affermazione, presente anche nella versione precedente di questo articolo: die() chiuderebbe la connessione, exit() la lascerebbe aperta. È falso. Sono due scritture dello stesso costrutto del linguaggio, e la documentazione PHP lo dice esplicitamente.
Verifica: due file identici a parte una parola, serviti dallo stesso 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.10Le due risposte sono identiche byte per byte, header Date a parte. Scegliere tra le due è una questione di stile. exit è più diffuso nel codice moderno, e scriverlo senza parentesi funziona lo stesso.
La trappola degli header già inviati
È l’errore più frequente su questo argomento. Basta un byte scritto prima di header(), uno spazio prima di <?php, una riga vuota dopo ?> in un file incluso, un echo di debug, un byte BOM in testa al file, e il redirect diventa impossibile.
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 3Il messaggio indica tra parentesi il file e la riga colpevoli. Per verificare prima di agire:
if (headers_sent($fichier, $ligne)) {
error_log("Sortie déjà commencée dans {$fichier}:{$ligne}");
// ripiego: redirect lato browser
echo '<meta http-equiv="refresh" content="0;url=/nouvelle-page">';
exit;
}
header('Location: /nouvelle-page', true, 303);
exit;Due regole evitano il problema alla radice: non chiudere mai un file PHP con ?> e non stampare niente prima di aver deciso la risposta.
L’open redirect
È la vera falla di sicurezza di questo argomento, e manca nella maggior parte dei tutorial. Il classico «torna alla pagina precedente dopo il login» si scrive spesso così:
// Vulnerabile
header('Location: ' . $_GET['retour']);
exit;A quel punto un attaccante manda un link della forma https://votre-site.com/connexion.php?retour=https://site-pirate.example. Il link porta il tuo nome di dominio, supera i filtri della posta e tranquillizza la vittima, che atterra su una copia della tua pagina di login. È il meccanismo di base di buona parte delle campagne di phishing.
La correzione non consiste nel filtrare i caratteri, ma nell’accettare solo destinazioni interne.
<?php
declare(strict_types=1);
/**
* Consente solo un percorso interno. Ogni URL assoluto viene scartato.
*/
function destinationSure(string $demande, string $defaut = '/'): string
{
// Caratteri di controllo: tentativo di header injection
if ($demande === '' || preg_match('/[\x00-\x1F\x7F]/', $demande) === 1) {
return $defaut;
}
// Una sola «/» in testa, non seguita da «/» né da «\»
if (!preg_match('#^/(?![/\\\\])#', $demande)) {
return $defaut;
}
$parties = parse_url($demande);
if ($parties === false || isset($parties['scheme']) || isset($parties['host'])) {
return $defaut;
}
// Nessuna risalita di percorso
if (str_contains($parties['path'] ?? '', '../')) {
return $defaut;
}
return $demande;
}
function redirigerVers(string $demande, string $defaut = '/'): never
{
header('Location: ' . destinationSure($demande, $defaut), true, 303);
exit;
}La nostra prima versione di questa funzione controllava solo il doppio slash iniziale. Le prove hanno mostrato che lasciava passare tre famiglie di valori, e ognuna delle righe aggiunte serve a chiuderle:
/site-pirate.example, i browser trattano il backslash come uno slash in questa posizione. È l’aggiramento più comune di un controllo che guarda solo//./okrnX-Injecte: 1—header()lo rifiuterebbe, ma è meglio scartare il valore prima di arrivare lì./ok/../../admin, una risalita di percorso che raggiunge una zona non prevista.
I quindici valori passati alla versione finale:
| Valore ricevuto | Destinazione |
|---|---|
/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 e /ok/../../admin | / |
/okrnX-Injecte: 1 | / |
mon-compte (senza slash) e stringa vuota | / |
Se servono davvero destinazioni esterne, l’unica forma sicura è una whitelist di domini, confrontata sull’host estratto da parse_url(), mai con str_contains().
L’header injection
Un a capo dentro il valore di un header permetteva un tempo di aggiungerne altri. PHP filtra gli a capo in header() dalla versione 5.1.2 e solleva un errore. Non serve fare altro, ma è un motivo in più per non costruire gli header concatenando dati dell’utente.
Quando non fare il redirect in PHP
Un redirect permanente da URL a URL non ha bisogno di PHP. Farlo a livello di server è più veloce, la richiesta non sveglia l’interprete, e sopravvive a un guasto dell’applicazione.
Redirect 301 /ancienne-page /nouvelle-page
# Forzare HTTPS
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]location = /ancienne-page {
return 301 /nouvelle-page;
}PHP resta la scelta giusta quando la destinazione dipende da uno stato: sessione, permessi, esito di un’elaborazione, lingua negoziata.
Da ricordare
header('Location: …')poiexit, sempre entrambi.- 303 dopo un form, 301 solo quando è definitivo.
die()edexit()sono identici, misurato e verificato.- Una destinazione che arriva dall’utente va validata sui percorsi interni, rifiutando gli URL assoluti e il doppio slash.
Il 303 dopo un form va spesso insieme a una conferma di ricezione: vedi inviare un’email con PHP. Se la destinazione va calcolata lato browser invece che lato server, vedi passare variabili da PHP a JavaScript. Per il redirect che segue l’upload di un file, vedi upload di un file in PHP.
Vedi anche l’hub Sviluppo web e la connessione a un database in PHP.
Errori frequenti
Date a parte.header() non interrompe niente. Il contenuto che segue parte nel corpo della risposta: invisibile nel browser, ben presente per curl, un proxy o un bot.header('Location: ' . $_GET['retour']) permette di fabbricare un link di phishing che porta il tuo nome di dominio. La destinazione va validata come percorso interno.//site-pirate.example è un URL relativo al protocollo. E passa anche /site-pirate.example: i browser trattano il backslash come uno slash in questa posizione.<?php, una riga vuota dopo ?> o un byte BOM. Non chiudere mai un file PHP con ?>.

