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.
Double-submit cookie
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 seSecure,HttpOnlyeSameSiteestã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-Typearbitrário: APIs que processamapplication/jsonmas também aceitamapplication/x-www-form-urlencodedpermitem 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
- Identificar todas as ações que mudam estado (não só formulários: também endpoints de API).
- Para cada uma, remover/alterar o token e verificar se a requisição ainda é aceita.
- Inspecionar
SameSitenos cookies de sessão e o tratamento deOrigin/Referer. - Montar uma PoC cross-site real (form auto-submit ou
fetch) e confirmar a execução com a sessão da vítima. - 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(ouStrict) +HttpOnly+Secure. - Verificação de
Origin/Refererem 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.