CSRF (Cross-Site Request Forgery), também chamado de session riding, explora um detalhe simples de como a web funciona: 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 saia. Se a sua aplicação autoriza uma ação que muda estado confiando só no cookie de sessão, um site malicioso consegue disparar essa ação em nome da vítima, sem nunca ver o cookie e sem precisar dele.
Basta que o usuário esteja autenticado na sua aplicação e abra uma página controlada pelo atacante. Ele não vê nada acontecer.
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 sozinho. 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. Daí ele dispara um “esqueci minha senha” e assume a conta.
Note que o atacante nunca leu o cookie. Ele só fez o navegador da vítima usá-lo por ele.
CSRF via GET é ainda mais fácil
Se uma ação que muda estado for exposta via GET, nem formulário é preciso. Uma imagem qualquer já dispara o ataque:
<img src="https://app.exemplo.com/conta/excluir?confirm=true" />
Daí a regra de ouro: GET nunca deve alterar estado. Requisições GET precisam ser seguras e idempotentes.
Defesa 1: tokens anti-CSRF
A proteção clássica é exigir, em toda requisição que muda estado, um valor secreto que o atacante não tem como conhecer nem 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 fica sem como preencher o campo. Use comparação em tempo constante para não vazar o token por timing.
Double-submit cookie
Variante sem estado no servidor: o token vai tanto em um cookie quanto em um campo do formulário (ou cabeçalho), e o servidor confere se os dois batem. Funciona porque o atacante não consegue ler nem ajustar o cookie de outra origem. Um cuidado: a técnica fica frágil se subdomínios não confiáveis puderem gravar cookies no domínio pai. Nesse cenário, assine o token com HMAC para impedir a forja.
Defesa 2: cookies SameSite
O atributo SameSite diz ao navegador para não enviar o cookie em requisições cross-site, o que ataca 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). Isso 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, mas pode atrapalhar a UX, porque quem chega de um link externo aparece deslogado.SameSite=None; Secure: necessário para cenários cross-site legítimos, como SSO e widgets. É justamente o modo que reabre a porta para CSRF e volta a exigir tokens.
SameSite é uma ótima camada de defesa em profundidade, mas não substitui tokens. Navegadores antigos não o respeitam, e os modos têm nuances entre si. 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, porque 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, só que o servidor nunca o confere. É segurança de mentira.
- Token global, não atrelado à sessão: um token estático compartilhado entre usuários serve tão pouco 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 tem.
- Aceitar
Content-Typearbitrário: APIs que processamapplication/jsonmas também aceitamapplication/x-www-form-urlencodeddeixam um form HTML simples disparar a ação. - GET que muda estado: logout, exclusão e toggles via GET continuam aparecendo o tempo todo.
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 nos lembra de que “estar autenticado” e “ter consentido” são coisas diferentes. Uma defesa madura empilha token, SameSite e checagem de origem, de modo que nenhuma falha isolada consiga reabrir a porta sozinha.


