PHP CSRF e Segurança de Formulários
A Ana já protege a lojinha contra scripts injetados (XSS) e contra SQL malicioso (prepared statements). Falta um ataque mais sorrateiro, em que o golpista nem chega a tocar no site dela: ele faz o navegador da própria vítima enviar um formulário. Esse ataque se chama CSRF (Cross-Site Request Forgery, "falsificação de requisição entre sites"). Nesta aula você vai ver o ataque acontecer de verdade e protegê-lo com três peças: um token, um campo oculto e o hash_equals.
- Como funciona um ataque CSRF, passo a passo, e por que estar logado é o que o torna possível.
- Gerar um token secreto com
random_bytese guardá-lo na sessão. - Colocar o token num campo oculto do formulário e conferi-lo com
hash_equals. - O que o atributo
SameSitedos cookies faz, e por que ele ajuda mas não substitui o token. - As regras de ouro: nunca mudar dados com
GET, token em todo formulário que altera algo.
A ideia: o bilhete com a assinatura certa
Imagine que o banco da Ana aceita qualquer bilhete que chegue com a assinatura dela. Um golpista não consegue falsificar a assinatura, mas descobre um truque: ele escreve "transfiram R$ 500 para o golpista" num papel e deixa em cima da mesa da Ana, no meio de outros papéis que ela vai assinar sem ler. A assinatura é verdadeira. O pedido, não.
Na web, a "assinatura" é o cookie de sessão. Depois que a Ana faz login na lojinha, o navegador dela manda esse cookie automaticamente em toda requisição para a lojinha, não importa de onde a requisição partiu. Se um site do golpista fizer o navegador da Ana enviar um formulário para a lojinha, o cookie vai junto, e a lojinha acha que foi a Ana.
- A Ana faz login na lojinha. O PHP cria a sessão e o navegador guarda o cookie
PHPSESSID. - Em outra aba, sem sair da lojinha, a Ana abre um link "imperdível" que recebeu. É o site do golpista.
- A página do golpista tem um formulário invisível que se envia sozinho para
localhost:8000/alterar-email.php. O navegador manda o cookie da lojinha junto. A lojinha confere a sessão, vê "usuária ana" e troca o e-mail da conta para o do golpista. Depois é só ele pedir "esqueci minha senha".
Repare: o golpista não roubou senha nem cookie, e o site da lojinha não tem nenhum bug de XSS ou SQL. O problema é que a lojinha não sabe de onde veio o formulário.
O ataque, de verdade
Este é o alterar-email.php da lojinha, ainda sem proteção. Ele confere se há alguém logado e troca o e-mail:
<?php
session_start();
if (!isset($_SESSION['usuario'])) {
http_response_code(401);
exit("Faça login primeiro\n");
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$_SESSION['email'] = $_POST['email'];
echo "E-mail alterado para {$_SESSION['email']}\n";
}
E esta é a página do golpista, hospedada em outro site qualquer:
<!doctype html>
<html>
<body onload="document.forms[0].submit()">
<h1>Parabéns! Você ganhou um cupom!</h1>
<form action="http://localhost:8000/alterar-email.php" method="post" style="display:none">
<input name="email" value="golpista@exemplo.com">
</form>
</body>
</html>
- action="http://localhost:8000/alterar-email.php"
- O formulário aponta para a lojinha, não para o site do golpista. Qualquer página pode mandar um formulário para qualquer site.
- style="display:none"
- Invisível: a Ana só vê "Parabéns!".
- onload="document.forms[0].submit()"
- Um pouco de JavaScript que envia o formulário assim que a página carrega, sem clique nenhum.
Como este guia não abre navegador, a Ana reproduziu o que o navegador faria com o curl. O -c cookies.txt guarda o cookie recebido no login (como o navegador guarda) e o -b cookies.txt o envia de volta (como o navegador envia sozinho). O -H 'Origin: …' imita o cabeçalho que diz de qual site veio o formulário:
$ curl -i -c cookies.txt localhost:8000/login.php HTTP/1.1 200 OK Host: localhost:8000 Date: Sat, 17 Oct 2026 00:02:39 GMT Connection: close X-Powered-By: PHP/8.5.8 Set-Cookie: PHPSESSID=6ceb8c39b417bf0769562ed04e51654a; path=/ Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: no-store, no-cache, must-revalidate Pragma: no-cache Content-type: text/html; charset=UTF-8 Logada como ana $ curl -b cookies.txt -d 'email=golpista@exemplo.com' -H 'Origin: http://site-do-golpista.example' localhost:8000/alterar-email.php E-mail alterado para golpista@exemplo.com
Pronto: com o cookie da Ana, o pedido vindo "do site do golpista" foi aceito. Sem o cookie (alguém que não está logado), não funciona:
$ curl -d 'email=golpista@exemplo.com' localhost:8000/alterar-email.php Faça login primeiro
É isso que torna o CSRF perigoso: ele ataca justamente quem está logado. Qualquer ação que um usuário logado pode fazer com um formulário (trocar e-mail ou senha, fazer um pedido, apagar uma conta, publicar um post) pode ser disparada assim.
A defesa: um token que só a lojinha conhece
O site do golpista consegue enviar o formulário, mas não consegue ler as páginas da lojinha (o navegador proíbe um site de ler a resposta de outro). Então a lojinha coloca em cada formulário um número secreto e aleatório, o token CSRF, e só aceita o pedido se o número voltar certo. O golpista não tem como saber o número, então o formulário falso chega sem ele.
São três peças:
| Peça | Onde | Código |
|---|---|---|
| 1. Criar o token | Na sessão, uma vez | $_SESSION['csrf'] = bin2hex(random_bytes(32)); |
| 2. Mandar o token | Campo oculto em todo formulário que altera algo | <input type="hidden" name="csrf" value="<?= $_SESSION['csrf'] ?>"> |
| 3. Conferir o token | No começo de quem recebe o POST | hash_equals($_SESSION['csrf'], $_POST['csrf']) |
Peça 1: criar o token
<?php
require __DIR__ . '/inicio.php';
if (!isset($_SESSION['usuario'])) {
http_response_code(401);
exit("Faça login primeiro\n");
}
if (empty($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32));
}
?>
<p>E-mail atual: <?= htmlspecialchars($_SESSION['email']) ?></p>
<form method="post" action="alterar-email.php">
<input type="hidden" name="csrf" value="<?= $_SESSION['csrf'] ?>">
<input type="email" name="email" required>
<button>Alterar e-mail</button>
</form>
- require __DIR__ . '/inicio.php'
- Um arquivo que configura o cookie e chama
session_start(). Você o verá na seção sobreSameSite. - if (empty($_SESSION['csrf']))
- Cria o token só se ainda não existir: um token por sessão, o mesmo para todos os formulários.
- random_bytes(32)
- 32 bytes aleatórios de verdade, próprios para segurança (diferente de
rand()oumt_rand(), que são previsíveis). - bin2hex(…)
- Transforma os bytes (que podem ser caracteres invisíveis) em texto hexadecimal: 64 caracteres de
0-9ea-f, seguros para colocar num HTML.
Peça 2: o campo oculto
<input type="hidden"> é um campo que não aparece na tela, mas é enviado com o formulário como qualquer outro. Veja o que a lojinha devolve para a Ana logada (o login.php agora também usa o inicio.php):
$ curl -i -c cookies.txt localhost:8000/login.php
HTTP/1.1 200 OK
Host: localhost:8000
Date: Sat, 17 Oct 2026 00:02:46 GMT
Connection: close
X-Powered-By: PHP/8.5.8
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Set-Cookie: PHPSESSID=e9622eb6ec5b671dd698792ddbd6eb74; path=/; HttpOnly; SameSite=Lax
Content-type: text/html; charset=UTF-8
Logada como ana
$ curl -b cookies.txt localhost:8000/conta.php
<p>E-mail atual: ana@lojinha.com</p>
<form method="post" action="alterar-email.php">
<input type="hidden" name="csrf" value="142c8d20e0ebe6148d7b666822c12df6804b2ff5240a1cc414eecb720f958b5b">
<input type="email" name="email" required>
<button>Alterar e-mail</button>
</form>
O token é aquele número de 64 caracteres. Ele é diferente para cada sessão: outra pessoa (ou a mesma Ana, em outro login) recebe outro. O value não precisa de htmlspecialchars porque o token só tem 0-9a-f, que nós mesmos geramos.
Peça 3: conferir com hash_equals
<?php
require __DIR__ . '/inicio.php';
if (!isset($_SESSION['usuario'])) {
http_response_code(401);
exit("Faça login primeiro\n");
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit("Use o formulário\n");
}
$tokenSessao = $_SESSION['csrf'] ?? '';
$tokenEnviado = $_POST['csrf'] ?? '';
if (!is_string($tokenEnviado) || $tokenSessao === '' || !hash_equals($tokenSessao, $tokenEnviado)) {
http_response_code(403);
exit("Pedido recusado: token CSRF inválido\n");
}
$_SESSION['email'] = $_POST['email'];
echo 'E-mail alterado para ', htmlspecialchars($_SESSION['email']), "\n";
| Linha | O que faz |
|---|---|
!== 'POST' → 405 | Só aceita POST. Ações que mudam dados nunca devem funcionar por GET (um simples <img src="…/alterar-email.php?email=…"> faria um GET). |
$_SESSION['csrf'] ?? '' | O token guardado. Se a sessão não tiver token (o usuário nunca abriu o formulário), vale ''. |
$_POST['csrf'] ?? '' | O token que veio no formulário. Formulário falso = não veio = ''. |
!is_string($tokenEnviado) | Recusa se alguém mandar um array (csrf[]=x), que faria o hash_equals dar erro fatal (veja os erros no fim da aula). |
$tokenSessao === '' | Sem token na sessão, não há o que comparar: recusa. |
hash_equals($tokenSessao, $tokenEnviado) | Compara os dois textos. Devolve true se forem idênticos. |
http_response_code(403) | 403 Forbidden: "proibido". |
Por que hash_equals e não ===?
O === compara os textos letra por letra e para na primeira diferença. Um token que começa errado é recusado um pouquinho mais rápido que um que só erra no fim. A diferença é de bilionésimos de segundo, mas medindo milhares de tentativas um atacante consegue descobrir o token letra por letra (isso se chama ataque de tempo, timing attack). O hash_equals ("hashes iguais?") foi feito para comparar segredos: ele sempre gasta o mesmo tempo, esteja o erro onde estiver. Regra: segredo se compara com hash_equals (tokens, assinaturas, códigos de confirmação). Para senhas, quem compara é o password_verify (aula Senhas). A ordem importa: primeiro o valor conhecido (o da sessão), depois o que veio do usuário.
O ataque de novo
$ curl -i -b cookies.txt -d 'email=golpista@exemplo.com' localhost:8000/alterar-email.php HTTP/1.1 403 Forbidden Host: localhost:8000 Date: Sat, 17 Oct 2026 00:02:46 GMT Connection: close X-Powered-By: PHP/8.5.8 Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: no-store, no-cache, must-revalidate Pragma: no-cache Content-type: text/html; charset=UTF-8 Pedido recusado: token CSRF inválido $ curl -b cookies.txt -d 'csrf=123456&email=golpista@exemplo.com' localhost:8000/alterar-email.php Pedido recusado: token CSRF inválido
Mesmo com o cookie da Ana, o pedido sem token (o formulário do golpista) e o pedido com um token chutado foram recusados. E a Ana de verdade, usando o formulário da lojinha? O navegador manda o campo oculto junto. Com o curl, a Ana pega o token da página (o grep -o separa a sequência de 64 caracteres hexadecimais) e o envia:
$ token=$(curl -s -b cookies.txt localhost:8000/conta.php | grep -o '[0-9a-f]\{64\}')
$ curl -b cookies.txt -d "csrf=$token&email=ana.souza@lojinha.com" localhost:8000/alterar-email.php
E-mail alterado para ana.souza@lojinha.com
$ curl -b cookies.txt localhost:8000/conta.php
<p>E-mail atual: ana.souza@lojinha.com</p>
<form method="post" action="alterar-email.php">
<input type="hidden" name="csrf" value="142c8d20e0ebe6148d7b666822c12df6804b2ff5240a1cc414eecb720f958b5b">
<input type="email" name="email" required>
<button>Alterar e-mail</button>
</form>
Funcionou, e o token continua o mesmo durante a sessão.
Experimente: um formulário protegido
Abaixo está a página "Minha conta" inteira num arquivo só, com as três peças marcadas nos comentários. O editor roda em modo web, com sessão: o que você faz no formulário fica guardado entre um envio e outro.
Experimente: (1) Rode e abra a aba Código-fonte HTML para ver o campo oculto com o token. (2) Na aba Resultado, digite um e-mail novo e envie: deve aparecer "E-mail alterado!". (3) Agora imite o golpista: apague a linha do <input type="hidden" …>, rode de novo e envie o formulário. O pedido chega sem token e é recusado.
<?php
session_start();
$_SESSION['email'] ??= 'ana@lojinha.com';
// 1. Um token por sessão, criado na primeira visita
if (empty($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32));
}
$mensagem = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 3. Conferir o token antes de qualquer mudança
$tokenEnviado = $_POST['csrf'] ?? '';
if (!is_string($tokenEnviado) || !hash_equals($_SESSION['csrf'], $tokenEnviado)) {
http_response_code(403);
$mensagem = 'Pedido recusado: token CSRF inválido.';
} else {
$_SESSION['email'] = $_POST['email'] ?? $_SESSION['email'];
$mensagem = 'E-mail alterado!';
}
}
?>
<!doctype html>
<html lang="pt-BR">
<head><meta charset="utf-8"><title>Minha conta</title></head>
<body>
<h1>Minha conta</h1>
<p><strong><?= htmlspecialchars($mensagem) ?></strong></p>
<p>E-mail atual: <?= htmlspecialchars($_SESSION['email']) ?></p>
<form method="post">
<!-- 2. O token vai escondido no formulário -->
<input type="hidden" name="csrf" value="<?= $_SESSION['csrf'] ?>">
<input type="email" name="email" placeholder="novo e-mail" required>
<button>Alterar e-mail</button>
</form>
</body>
</html>
No computador da Ana, o mesmo arquivo servido pelo php -S responde assim a um POST sem token (o trecho do <body>):
$ curl -s -d 'email=golpista@exemplo.com' localhost:8000/index.php … <h1>Minha conta</h1> <p><strong>Pedido recusado: token CSRF inválido.</strong></p> <p>E-mail atual: ana@lojinha.com</p> …
SameSite: o navegador ajudando
Os cookies têm um atributo, SameSite ("mesmo site"), que diz ao navegador quando ele pode enviar o cookie em requisições que partiram de outro site. Compare o cabeçalho Set-Cookie das duas versões do login, lá em cima:
Set-Cookie: PHPSESSID=6ceb8c39b417bf0769562ed04e51654a; path=/ « padrão do PHP Set-Cookie: PHPSESSID=e9622eb6ec5b671dd698792ddbd6eb74; path=/; HttpOnly; SameSite=Lax « com o inicio.php
A diferença veio deste arquivo, incluído no começo de todas as páginas da versão protegida:
<?php
session_set_cookie_params([
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
- session_set_cookie_params([...])
- Configura o cookie da sessão. Precisa vir antes do
session_start(). - 'httponly' => true
- O JavaScript da página não consegue ler o cookie (dificulta roubá-lo por XSS). Não tem a ver com CSRF, mas é bom hábito.
- 'samesite' => 'Lax'
- O navegador não envia o cookie em POSTs vindos de outro site. O formulário escondido do golpista chegaria sem o cookie, ou seja, como alguém não logado.
- 'secure' => true
- (Não usado aqui porque o
php -Séhttp.) No site real, com HTTPS, acrescente: o cookie só viaja por conexão criptografada.
| Valor | O navegador envia o cookie em pedidos vindos de outro site? | Uso |
|---|---|---|
Strict | Nunca. Nem quando você clica num link para a lojinha vindo do e-mail (você chega "deslogado"). | Bancos, áreas administrativas. |
Lax | Só em navegação comum com GET (clicar num link). Em POST, imagens, iframes: não. | O bom padrão para a maioria dos sites. |
None | Sempre (exige também Secure). | Só quando outro site precisa mesmo usar a sua sessão (ex.: um widget incorporado). |
Para todo o site, sem mexer no código, o mesmo vale no php.ini: session.cookie_samesite = "Lax" e session.cookie_httponly = 1. Para cookies seus, o setcookie aceita a mesma opção: setcookie('tema', 'escuro', ['samesite' => 'Lax']) (veja Cookies).
curl não é um navegador e não respeita SameSite (por isso a demonstração acima precisou do token para ser barrada). Os navegadores respeitam, mas: navegadores antigos ignoram o atributo; Lax deixa passar GET (se alguma ação sua muda dados por GET, ela continua vulnerável); e "mesmo site" inclui subdomínios (um blog.lojinha.com invadido pode atacar loja.lojinha.com). Use os dois: SameSite=Lax como segunda camada, token como proteção principal.Formas diferentes e boas práticas
| Situação | O que fazer |
|---|---|
| Vários formulários no site | O mesmo token da sessão em todos. Crie uma função campoCsrf(): string que devolve o <input type="hidden"…> e outra conferirCsrf(): void que recusa com 403, e chame-as em todo lugar. |
| Depois do login | Chame session_regenerate_id(true) (troca o id da sessão) e gere um token novo. Assim um token de antes do login não serve depois. |
Requisições via JavaScript (fetch) | Mande o token num cabeçalho, como X-CSRF-Token, e confira com $_SERVER['HTTP_X_CSRF_TOKEN']. |
| Ações muito sensíveis (trocar senha, apagar conta) | Além do token, peça a senha atual de novo. |
| Frameworks | Laravel e Symfony já geram e conferem o token (no Laravel, a diretiva @csrf no formulário). Por baixo, é exatamente o que você fez aqui. |
| Camada extra | Navegadores modernos mandam o cabeçalho Sec-Fetch-Site (same-origin, cross-site…) e o Origin. Recusar POSTs com Sec-Fetch-Site: cross-site é uma boa defesa adicional. |
Regras de ouro: (1) GET só consulta, nunca altera; (2) todo POST que altera algo confere o token; (3) cookies de sessão com HttpOnly, SameSite=Lax e, em produção, Secure.
Quando dá errado
Sem o is_string, um atacante pode mandar o campo como array (csrf[]=x):
$ curl -b cookies.txt -d 'csrf[]=x&email=golpista@exemplo.com' localhost:8000/sem-is-string.php
<br />
<b>Fatal error</b>: Uncaught TypeError: hash_equals(): Argument #2 ($user_string) must be of type string, array given in /home/ana/csrf2/sem-is-string.php:17
Stack trace:
#0 /home/ana/csrf2/sem-is-string.php(17): hash_equals(Object(SensitiveParameterValue), Object(SensitiveParameterValue))
#1 {main}
thrown in <b>/home/ana/csrf2/sem-is-string.php</b> on line <b>17</b><br />
O que significa: no PHP, csrf[]=x no formulário vira $_POST['csrf'] = ['x']. O hash_equals só aceita textos e lançou um TypeError. O e-mail não foi trocado, mas a página quebrou mostrando o caminho do arquivo. Repare em Object(SensitiveParameterValue): o PHP esconde os argumentos do hash_equals no rastro do erro, para o token não vazar na mensagem.
Como consertar: confira o tipo antes, como no alterar-email.php protegido. Com o is_string, a resposta é a de sempre:
$ curl -b cookies.txt -d 'csrf[]=x&email=golpista@exemplo.com' localhost:8000/alterar-email.php Pedido recusado: token CSRF inválido
O que significa: o formulário foi enviado com um token que não bate com o da sessão. Causas comuns: o formulário não tem o campo oculto (esqueceu o <input type="hidden">); o name do campo é diferente do que o PHP lê (token × csrf); a sessão expirou (a página ficou aberta horas) ou foi trocada por um novo login; ou a página de destino não chamou session_start(), e então $_SESSION está vazio.
Como consertar: confira o código-fonte da página (o campo está lá, com o nome certo?) e se todos os arquivos começam a sessão. Para sessão expirada, mostre uma mensagem amigável ("sua sessão expirou, recarregue a página") em vez de só "403".
$ curl -b cookies.txt localhost:8000/alterar-email.php Use o formulário
O que significa: abrir a URL na barra de endereços faz um GET, e a página só aceita POST. Isso é proposital.
Como consertar: nada a consertar. Se você precisa de uma página de "confirmação", faça-a mostrar um formulário (com token) que envia o POST.
<?php
session_start();
session_set_cookie_params([
'httponly' => true,
'samesite' => 'Lax',
]);
$ curl -s localhost:8000/ordem.php <br /> <b>Warning</b>: session_set_cookie_params(): Session cookie parameters cannot be changed when a session is active (started from /home/ana/csrf2/ordem.php on line 2) in <b>/home/ana/csrf2/ordem.php</b> on line <b>3</b><br />
O que significa: "os parâmetros do cookie da sessão não podem ser mudados com uma sessão ativa (iniciada na linha 2)". O session_set_cookie_params foi chamado depois do session_start(), e o cookie ficou sem SameSite. Os parâmetros do cookie precisam ser definidos antes de a sessão começar.
Como consertar: centralize as duas chamadas num arquivo como o inicio.php, nesta ordem, e inclua-o no topo de todas as páginas.
Referência rápida
| O quê | Como |
|---|---|
| Gerar o token | $_SESSION['csrf'] = bin2hex(random_bytes(32)); (se ainda não existir) |
| Campo oculto | <input type="hidden" name="csrf" value="<?= $_SESSION['csrf'] ?>"> |
| Conferir | is_string($t) && hash_equals($_SESSION['csrf'], $t); senão http_response_code(403) |
| Comparar segredos | hash_equals($conhecido, $enviado) (tempo constante) |
| Cookie de sessão | session_set_cookie_params(['httponly' => true, 'samesite' => 'Lax', 'secure' => true]) antes do session_start() |
No php.ini | session.cookie_samesite = "Lax", session.cookie_httponly = 1, session.cookie_secure = 1 |
| Depois do login | session_regenerate_id(true); e token novo |
| Só POST altera | if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); … } |
Exercícios
1. Bytes aleatórios seguros
Complete a geração do token:
2. O campo que não aparece
Complete o tipo do campo que leva o token no formulário:
3. Comparar sem vazar tempo
Complete com a função que compara segredos em tempo constante:
4. Por que o ataque CSRF funciona contra um site sem token?
5. Com o cookie de sessão em SameSite=Lax, o que acontece com o formulário POST escondido no site do golpista?
6. Qual destas ações está errada?
Missão final: a primeira requisição desta caixa já é o ataque: um POST com email=golpista@exemplo.com e sem token, como o do formulário escondido. Do jeito que está, a página aceita e mostra "E-mail alterado para golpista@exemplo.com". Proteja a página com as três peças: crie o token na sessão com random_bytes, coloque o campo oculto no formulário e confira com hash_equals antes de trocar o e-mail, mostrando "Pedido recusado: token CSRF inválido." quando falhar. Depois de rodar, use o formulário de verdade para conferir que a troca legítima continua funcionando.
<?php
session_start();
$_SESSION['email'] ??= 'ana@lojinha.com';
$mensagem = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$_SESSION['email'] = $_POST['email'] ?? $_SESSION['email'];
$mensagem = 'E-mail alterado para ' . $_SESSION['email'];
}
?>
<!doctype html>
<html lang="pt-BR">
<head><meta charset="utf-8"><title>Minha conta</title></head>
<body>
<h1>Minha conta</h1>
<p><strong><?= htmlspecialchars($mensagem) ?></strong></p>
<p>E-mail atual: <?= htmlspecialchars($_SESSION['email']) ?></p>
<form method="post">
<input type="email" name="email" placeholder="novo e-mail" required>
<button>Alterar e-mail</button>
</form>
</body>
</html>
<?php
session_start();
$_SESSION['email'] ??= 'ana@lojinha.com';
if (empty($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32));
}
$mensagem = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$tokenEnviado = $_POST['csrf'] ?? '';
if (!is_string($tokenEnviado) || !hash_equals($_SESSION['csrf'], $tokenEnviado)) {
http_response_code(403);
$mensagem = 'Pedido recusado: token CSRF inválido.';
} else {
$_SESSION['email'] = $_POST['email'] ?? $_SESSION['email'];
$mensagem = 'E-mail alterado para ' . $_SESSION['email'];
}
}
?>
<!doctype html>
<html lang="pt-BR">
<head><meta charset="utf-8"><title>Minha conta</title></head>
<body>
<h1>Minha conta</h1>
<p><strong><?= htmlspecialchars($mensagem) ?></strong></p>
<p>E-mail atual: <?= htmlspecialchars($_SESSION['email']) ?></p>
<form method="post">
<input type="hidden" name="csrf" value="<?= $_SESSION['csrf'] ?>">
<input type="email" name="email" placeholder="novo e-mail" required>
<button>Alterar e-mail</button>
</form>
</body>
</html>
Resumo
- CSRF: um site do golpista faz o navegador de um usuário logado enviar um formulário para o seu site; o cookie de sessão vai junto automaticamente.
- Defesa principal: um token secreto por sessão (
bin2hex(random_bytes(32))), enviado num<input type="hidden">e conferido comhash_equalsantes de qualquer alteração. O golpista não consegue ler o token. - Confira também se o token é texto (
is_string) e responda403quando falhar. hash_equalscompara segredos em tempo constante;===vaza informação pelo tempo.SameSite=Lax(comHttpOnlye, em HTTPS,Secure) faz o navegador não mandar o cookie em POSTs de outros sites: ótima segunda camada, mas não substitui o token.GETnunca altera dados.