CSRF: o ataque que usa sua sessão contra você

Entenda como um site malicioso explora um cookie de sessão já autenticado pra disparar uma ação sem o usuário saber, e como token CSRF e o atributo SameSite do cookie previnem isso na prática.


Já expliquei aqui por que CORS bloqueia leitura de resposta entre origens diferentes, e por que JWT ou sessão guardam quem é o usuário logado. CSRF explora exatamente o espaço entre esses dois conceitos: o navegador manda cookie de sessão automaticamente em qualquer requisição pra aquele domínio, não importa de onde essa requisição partiu, e é justamente esse comportamento "automático" que um ataque de CSRF usa contra o próprio usuário.

O cenário do ataque

Imagina que você está logado no site do seu banco, com um cookie de sessão válido guardado no navegador. Num horário qualquer, você abre, numa outra aba, um site completamente diferente, sem relação nenhuma com o banco. Esse site tem um formulário escondido, invisível, que se auto-envia assim que a página carrega:

<form action="https://meubanco.com/transferir" method="POST" id="formMalicioso">
  <input type="hidden" name="destino" value="conta-do-atacante" />
  <input type="hidden" name="valor" value="5000" />
</form>
<script>
  document.getElementById("formMalicioso").submit();
</script>

Essa requisição parte do navegador da vítima, pro domínio real do banco. O navegador, sem questionar, já anexa o cookie de sessão daquele domínio automaticamente, porque é exatamente isso que um cookie faz: ele viaja com qualquer requisição pro domínio que o criou, não importa qual página disparou essa requisição. Do ponto de vista do servidor do banco, essa chamada parece perfeitamente legítima: tem cookie de sessão válido, usuário autenticado, tudo nos conformes. O servidor não tem como saber, só pelo cookie, que quem realmente iniciou aquela ação foi um site diferente, sem o consentimento do usuário.

Por que isso é diferente de CORS

Aqui mora a confusão mais comum: CORS não protege contra isso. CORS bloqueia o navegador de deixar o JavaScript de outra origem ler a resposta de uma requisição cross-origin, mas não impede a requisição de ser enviada e processada do lado do servidor. No ataque de CSRF, o atacante nem precisa ler a resposta, só precisa que a ação (transferir dinheiro, mudar e-mail cadastrado, excluir conta) seja executada. O dano já aconteceu no momento em que o servidor processa a requisição, independente de quem consegue ver a resposta dela depois.

Token CSRF: a defesa clássica

A solução tradicional é exigir, além do cookie de sessão, um token extra que só o site legítimo consegue incluir na requisição, porque esse token é gerado pelo próprio servidor e injetado na página de verdade, nunca disponível pra um site externo adivinhar ou copiar:

<form action="/transferir" method="POST">
  <input type="hidden" name="csrf_token" value="a1b2c3d4e5f6" />
  <input type="text" name="valor" />
</form>

O servidor, ao processar essa requisição, confere se o csrf_token enviado bate com o que foi gerado pra aquela sessão específica:

app.post("/transferir", (req, res) => {
  if (req.body.csrf_token !== req.session.csrfToken) {
    return res.status(403).json({ erro: "token CSRF inválido" });
  }
 
  // processa a transferência normalmente
});

Um formulário malicioso hospedado em outro site não tem como conhecer esse token, porque ele nunca foi exposto pra fora do HTML da página real do banco. Mesmo que o cookie de sessão viaje automaticamente, a requisição falha por não ter o token correto.

Além do token, os navegadores modernos oferecem um atributo direto no cookie que ataca o problema na raiz: SameSite. Ele controla em quais situações o cookie é enviado numa requisição cross-site:

Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly

Com SameSite=Strict, o cookie simplesmente não é enviado em nenhuma requisição originada de outro site, inclusive aquele formulário malicioso do exemplo. Isso elimina boa parte dos ataques de CSRF sem precisar de token nenhum, só configurando esse atributo corretamente.

Existe uma variação mais flexível, SameSite=Lax, que permite o cookie em navegação direta (clicar num link que leva pro site) mas ainda bloqueia em requisições automáticas disparadas por outro site, como POST de formulário ou fetch. É o valor padrão adotado pela maioria dos navegadores hoje, um meio-termo que já reduz bastante a superfície de ataque sem quebrar casos de uso legítimos de navegação normal.

As duas camadas, juntas

Na prática, a defesa mais robusta combina os dois: SameSite configurado corretamente no cookie como primeira linha de proteção, e token CSRF como camada extra pra ações realmente sensíveis (mudança de senha, transferência, exclusão de conta), cobrindo cenários em que, por algum motivo, a configuração do cookie sozinha não seria suficiente.

Fechando

CSRF explora uma característica normal do navegador, cookie viajando automaticamente pra seu domínio, pra fazer o servidor confiar numa requisição que o usuário nunca pretendeu enviar. A defesa não depende de confiar na origem da requisição (isso é papel do CORS, pra outro problema), depende de garantir que só quem realmente está na página legítima consegue montar uma requisição válida, seja com token CSRF, seja restringindo quando o próprio cookie de sessão é enviado.

Direto na sua
caixa de entrada.

Um aviso por e-mail sempre que eu publicar um post novo. Sem spam, sem newsletter chata, só isso.