
header('Location: /destino') seguido de exit, sempre os dois. Sem código explícito, o PHP envia um 302, usa 301 para uma mudança definitiva e 303 depois de tratar um formulário. Um destino fornecido pelo utilizador tem de ser validado, senão abres um redirecionamento para um site terceiro.
Redirecionar em PHP cabe em duas linhas. O que merece um artigo é a escolha do código HTTP, a paragem do script logo a seguir e a falha de redirecionamento aberto que aparece assim que o destino vem de dados enviados pelo utilizador.
O redirecionamento mínimo
<?php
header('Location: /nouvelle-page');
exit;Dois pontos mal compreendidos com frequência. Primeiro, o Location aceita tanto um URL absoluto como um caminho relativo à raiz: ambos funcionam em todos os browsers atuais. Depois, sem código explícito, o PHP envia um 302 Found.
Escolher o código certo
| Código | Significado | Método preservado | Quando usar |
|---|---|---|---|
| 301 | Movido definitivamente | não (o POST passa a GET) | Um URL mudou de vez |
| 302 | Encontrado, temporário | não (o POST passa a GET) | Predefinição histórica, a evitar escrever |
| 303 | Ver noutro sítio | não, nunca | Depois de tratar um formulário |
| 307 | Temporário | sim | Manutenção, mudança provisória |
| 308 | Permanente | sim | URL mudado, preservando o POST |
// Uma página mudou de sítio definitivamente
header('Location: https://exemple.com/nouvelle-adresse', true, 301);
exit;
// Depois de tratar um formulário: evita o reenvio do POST ao recarregar
header('Location: /merci', true, 303);
exit;O 303 depois de um formulário tem nome: Post/Redirect/Get. Sem ele, atualizar a página faz o browser reenviar o formulário, e a encomenda segue duas vezes.
O 301 fica em cache no browser, às vezes durante muito tempo. Um redirecionamento permanente colocado por engano é penoso de retirar: os visitantes que o receberam continuam a ser redirecionados sem sequer contactar o servidor. Na dúvida, começa por um 302 ou um 307.
Parar sempre o script
O header() coloca um cabeçalho, não interrompe nada. O código que vem a seguir é executado e o que ele produz segue no corpo da resposta.
<?php
header('Location: /connexion', true, 301);
echo "CONTENU QUI FUIT"; // saiu à mesmaMedido com o Apache e o 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 »O browser segue o redirecionamento e não mostra esse corpo, o que torna o problema invisível. Mas ele é mesmo transmitido: o curl, um proxy, um registo intermédio ou um robô veem-no. Numa página que redireciona um visitante não autenticado, é exatamente o conteúdo que se queria proteger que sai para a rede.
die() e exit() são a mesma coisa
Circula muito uma afirmação, presente inclusive na versão anterior deste artigo: o die() fecharia a ligação e o exit() mantê-la-ia aberta. É falso. São duas grafias da mesma construção da linguagem, e a documentação do PHP di-lo explicitamente.
Verificação: dois ficheiros idênticos a menos de uma palavra, servidos pelo mesmo 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.10As duas respostas são idênticas byte a byte, tirando o cabeçalho Date. Escolher entre as duas é uma questão de estilo. O exit é mais comum no código moderno, e escrevê-lo sem parênteses também funciona.
A armadilha dos cabeçalhos já enviados
É o erro mais frequente neste tema. O mais pequeno byte escrito antes do header(), um espaço antes de <?php, uma linha vazia depois de ?> num ficheiro incluído, um echo de depuração, um byte BOM no início do ficheiro, torna o redirecionamento impossível.
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 3A mensagem indica entre parênteses o ficheiro e a linha culpados. Para testar antes de agir:
if (headers_sent($fichier, $ligne)) {
error_log("Sortie déjà commencée dans {$fichier}:{$ligne}");
// alternativa: redirecionamento do lado do browser
echo '<meta http-equiv="refresh" content="0;url=/nouvelle-page">';
exit;
}
header('Location: /nouvelle-page', true, 303);
exit;Duas regras evitam o problema na origem: nunca fechar um ficheiro PHP com ?> e não mostrar nada antes de ter decidido qual é a resposta.
O redirecionamento aberto
É a verdadeira falha de segurança deste tema, e está ausente da maior parte dos tutoriais. O padrão «voltar à página anterior depois do login» costuma escrever-se assim:
// Vulnerável
header('Location: ' . $_GET['retour']);
exit;Um atacante envia então uma ligação com a forma https://votre-site.com/connexion.php?retour=https://site-pirate.example. A ligação leva o teu nome de domínio, passa os filtros de e-mail e tranquiliza a vítima, que aterra numa cópia da tua página de login. É o mecanismo de base de boa parte das campanhas de phishing.
A correção não passa por filtrar caracteres, mas por aceitar apenas destinos internos.
<?php
declare(strict_types=1);
/**
* Só autoriza um caminho interno. Qualquer URL absoluto é descartado.
*/
function destinationSure(string $demande, string $defaut = '/'): string
{
// Caracteres de controlo: tentativa de injeção de cabeçalho
if ($demande === '' || preg_match('/[\x00-\x1F\x7F]/', $demande) === 1) {
return $defaut;
}
// Uma única « / » no início, não seguida de « / » nem de « \ »
if (!preg_match('#^/(?![/\\\\])#', $demande)) {
return $defaut;
}
$parties = parse_url($demande);
if ($parties === false || isset($parties['scheme']) || isset($parties['host'])) {
return $defaut;
}
// Sem subida de caminho
if (str_contains($parties['path'] ?? '', '../')) {
return $defaut;
}
return $demande;
}
function redirigerVers(string $demande, string $defaut = '/'): never
{
header('Location: ' . destinationSure($demande, $defaut), true, 303);
exit;
}A nossa primeira versão desta função só testava a barra dupla inicial. Os testes mostraram que deixava passar três famílias de entradas, o que justifica cada uma das linhas acrescentadas:
/site-pirate.example, os browsers tratam a barra invertida como uma barra nesta posição. É a forma mais comum de contornar um controlo que só olha para//./okrnX-Injecte: 1, oheader()recusaria este valor, mas mais vale descartá-lo antes de lá chegar./ok/../../admin, uma subida de caminho que chega a uma zona não prevista.
As quinze entradas passadas à versão final:
| Valor recebido | Destino |
|---|---|
/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 (sem barra) e string vazia | / |
Se forem mesmo precisos destinos externos, a única forma segura é uma lista branca de domínios, comparada com o host extraído por parse_url(), nunca com str_contains().
A injeção no cabeçalho
Uma quebra de linha no valor de um cabeçalho permitia antigamente acrescentar outros. O PHP filtra as quebras de linha no header() desde a versão 5.1.2 e lança um erro. Não há mais nada a fazer, mas é mais uma razão para não construir cabeçalhos concatenando dados do utilizador.
Quando não redirecionar em PHP
Um redirecionamento permanente de URL para URL não precisa de PHP. Fazê-lo ao nível do servidor é mais rápido, o pedido não acorda o interpretador, e sobrevive a uma avaria da aplicação.
Redirect 301 /ancienne-page /nouvelle-page
# Forçar HTTPS
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]location = /ancienne-page {
return 301 /nouvelle-page;
}O PHP continua indicado quando o destino depende de um estado: sessão, permissões, resultado de um tratamento, língua negociada.
A reter
header('Location: …')e depoisexit, sempre os dois.- 303 depois de um formulário, 301 só quando é definitivo.
die()eexit()são idênticos, medido e verificado.- Um destino vindo do utilizador valida-se contra os caminhos internos, recusando os URL absolutos e a barra dupla.
O 303 depois de um formulário anda muitas vezes a par de um aviso de receção: vê enviar um e-mail com PHP. Se o destino tiver de ser calculado do lado do browser em vez do servidor, vê passar variáveis de PHP para JavaScript. Para o redirecionamento que se segue ao envio de um ficheiro, vê fazer upload de um ficheiro em PHP.
Vê também o hub Desenvolvimento web e a ligação a uma base de dados em PHP.
Erros frequentes
Date.header() não interrompe nada. O conteúdo que vem a seguir segue no corpo da resposta: invisível no browser, bem presente para o curl, um proxy ou um robô.header('Location: ' . $_GET['retour']) permite fabricar uma ligação de phishing com o teu nome de domínio. O destino tem de ser validado como caminho interno.//site-pirate.example é um URL relativo ao protocolo. E /site-pirate.example também passa: os browsers tratam a barra invertida como uma barra nesta posição.<?php, uma linha vazia depois de ?> ou um byte BOM chegam para isso. Nunca fechar um ficheiro PHP com ?>.

