CSRF (Cross-Site Request Forgery), também chamado de session riding, explora um detalhe simples do funcionamento da web: o navegador anexa automaticamente os cookies da vítima a qualquer requisição para um domínio, não importa de onde a requisição parta. Se a sua aplicação confia somente no cookie de sessão para autorizar uma ação que muda estado, um site malicioso pode disparar essa ação em nome da vítima, sem nunca ver o cookie nem precisar dele.

O usuário só precisa estar autenticado na sua aplicação e visitar uma página controlada pelo atacante. O resto acontece de forma invisível.

A anatomia do ataque

Imagine uma aplicação que troca o e-mail da conta com este formulário:

<form action="https://app.exemplo.com/conta/email" method="POST">
  <input name="email" value="novo@usuario.com" />
  <button>Salvar</button>
</form>

Se o servidor aceitar esse POST validando apenas o cookie de sessão, o atacante hospeda a seguinte página:

<!-- pagina-do-atacante.com -->
<form id="x" action="https://app.exemplo.com/conta/email" method="POST">
  <input type="hidden" name="email" value="atacante@evil.com" />
</form>
<script>document.getElementById('x').submit();</script>

Quando a vítima autenticada abre essa página, o formulário é enviado automaticamente. O navegador anexa o cookie de sessão de app.exemplo.com, o servidor vê uma requisição “válida” e troca o e-mail da conta para o do atacante — que, em seguida, dispara um “esqueci minha senha” e assume a conta.

Repare: o atacante nunca leu o cookie. Ele apenas fez o navegador da vítima usá-lo.

CSRF via GET é ainda mais fácil

Se uma ação que muda estado for exposta via GET, nem formulário é preciso — uma simples imagem dispara o ataque:

<img src="https://app.exemplo.com/conta/excluir?confirm=true" />

Por isso a regra de ouro: GET nunca deve alterar estado. Requisições GET devem ser seguras e idempotentes.

Defesa 1: tokens anti-CSRF

A proteção clássica é exigir, em cada requisição que muda estado, um valor secreto que o atacante não tem como conhecer ou adivinhar.

Synchronizer token pattern

O servidor gera um token aleatório por sessão (ou por requisição), incorpora-o no formulário e o valida no recebimento:

<form action="/conta/email" method="POST">
  <input type="hidden" name="csrf_token" value="b7f3c1e9a4d28f6e..." />
  <input name="email" value="novo@usuario.com" />
  <button>Salvar</button>
</form>
# Validação no servidor (pseudo-Python)
def handle_post(request, session):
    token_form = request.form.get("csrf_token")
    token_session = session.get("csrf_token")
    if not token_session or not constant_time_compare(token_form, token_session):
        abort(403)  # rejeita
    update_email(request.form["email"])

Como o site do atacante não consegue ler o token (a same-origin policy impede a leitura da resposta de outra origem), ele não tem como preencher o campo. Use comparação em tempo constante para evitar vazamento por timing.

Variante sem estado no servidor: o token é enviado tanto em um cookie quanto em um campo do formulário/cabeçalho; o servidor confere se os dois batem. Funciona porque o atacante não consegue ler nem ajustar o cookie de outra origem. Atenção: essa técnica fica frágil se subdomínios não confiáveis puderem gravar cookies no domínio pai — nesse caso, assine o token (HMAC) para impedir forja.

Defesa 2: cookies SameSite

O atributo SameSite instrui o navegador a não enviar o cookie em requisições cross-site, cortando a raiz do CSRF:

Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax
  • SameSite=Lax (padrão moderno na maioria dos navegadores): o cookie acompanha navegação de topo via GET, mas não vai em POST cross-site nem em sub-requisições (imagens, iframes). Já neutraliza a maioria dos vetores. Confira se Secure, HttpOnly e SameSite estão setados nos seus cookies com o Analisador de Security Headers.
  • SameSite=Strict: o cookie nunca é enviado em contexto cross-site, nem em links. Mais seguro, porém pode prejudicar a UX (quem chega de um link externo aparece deslogado).
  • SameSite=None; Secure: necessário para cenários cross-site legítimos (SSO, widgets) — e justamente o que reabre a porta para CSRF, exigindo tokens.

SameSite é uma excelente camada de defesa em profundidade, mas não substitui tokens: navegadores antigos não o respeitam, e há nuances entre os modos. Use os dois.

Defesa 3: verificação de Origin / Referer

Em requisições que mudam estado, o servidor pode conferir se o cabeçalho Origin (ou, na ausência, Referer) corresponde à própria aplicação:

ALLOWED = {"https://app.exemplo.com"}

def is_same_origin(request):
    origin = request.headers.get("Origin") or request.headers.get("Referer")
    return origin and any(origin.startswith(o) for o in ALLOWED)

É uma defesa robusta e barata para APIs, mas trate a ausência do cabeçalho com cuidado (alguns proxies o removem) — combine com token em vez de confiar só nela.

Erros comuns que vemos em pentests

  • Token presente, mas não validado: o campo existe no HTML, porém o servidor nunca o confere. Falsa sensação de segurança.
  • Token global, não atrelado à sessão: um token estático compartilhado entre usuários é tão útil quanto nenhum.
  • Apenas endpoints “óbvios” protegidos: o formulário de troca de senha tem token, mas o endpoint de API JSON que faz a mesma coisa, não.
  • Aceitar Content-Type arbitrário: APIs que processam application/json mas também aceitam application/x-www-form-urlencoded permitem que um form HTML simples dispare a ação.
  • GET que muda estado: logout, exclusão e toggles via GET continuam comuns.

Como testamos CSRF

  1. Identificar todas as ações que mudam estado (não só formulários: também endpoints de API).
  2. Para cada uma, remover/alterar o token e verificar se a requisição ainda é aceita.
  3. Inspecionar SameSite nos cookies de sessão e o tratamento de Origin/Referer.
  4. Montar uma PoC cross-site real (form auto-submit ou fetch) e confirmar a execução com a sessão da vítima.
  5. Avaliar impacto pela criticidade da ação sequestrável.

Checklist de mitigação

  • Token anti-CSRF por sessão, validado em toda ação que muda estado (web e API).
  • Comparação de token em tempo constante.
  • Cookies de sessão com SameSite=Lax (ou Strict) + HttpOnly + Secure.
  • Verificação de Origin/Referer em endpoints sensíveis.
  • Nenhuma ação que muda estado via GET.
  • Para SPAs/APIs: exigir cabeçalho custom (ex.: X-CSRF-Token) que o cross-site não consegue setar sem CORS.
  • Reautenticação em operações críticas (troca de e-mail, senha, MFA).

CSRF é um lembrete de que “estar autenticado” e “ter consentido” são coisas diferentes. A defesa madura combina três camadas — token, SameSite e checagem de origem — para que nenhuma falha isolada reabra a porta.