• CSRF - Cross-Site Request Forgery

    Se você já criou um formulário no Laravel, com certeza esbarrou naquela diretiva @csrf que a gente coloca sem nem pensar muito. Mas você já parou pra pensar o que ela realmente faz e por que, se você esquecer dela, o Laravel te devolve um belo de um erro 419 Page Expired? Pois é, tudo isso gira em torno de um ataque chamado CSRF, e é sobre ele que vamos falar.

    #O que é CSRF?

    CSRF significa Cross-Site Request Forgery, ou "falsificação de requisição entre sites". A ideia do ataque é simples e por isso mesmo perigosa: fazer o navegador da vítima enviar uma requisição para um site no qual ela já está autenticada, sem que ela perceba.

    O ponto central que torna esse ataque possível é o comportamento dos cookies no navegador. Quando você faz login em banco.com, o servidor te devolve um cookie de sessão. A partir daí, toda requisição que o seu navegador fizer para banco.com vai levar esse cookie junto, automaticamente. E aqui está o pulo do gato: o navegador não liga de onde a requisição partiu. Se um site totalmente diferente disparar uma requisição para banco.com, o cookie de sessão vai junto do mesmo jeito.

    #Como o ataque funciona na prática

    Imagina que o seu banco tem um endpoint de transferência que funciona assim:

     1<form action="https://banco.com/transferir" method="POST">
     2    <input type="text" name="valor">
     3    <input type="text" name="destino">
     4    <button>Transferir</button>
     5</form>
    

    Você está logado no seu banco em uma aba. Em outra aba, você abre um site qualquer que recebeu de um e-mail suspeito. O que você não sabe é que esse site malicioso tem escondido nele algo assim:

     1<form action="https://banco.com/transferir" method="POST" id="ataque">
     2    <input type="hidden" name="valor" value="10000">
     3    <input type="hidden" name="destino" value="conta-do-atacante">
     4</form>
     5
     6<script>
     7    document.getElementById('ataque').submit();
     8</script>
    

    Quando a página carrega, o JavaScript envia o formulário sozinho. O navegador faz a requisição para banco.com, e como é uma requisição para o domínio do banco, ele anexa o seu cookie de sessão automaticamente. Do ponto de vista do servidor do banco, é uma requisição perfeitamente autenticada, vinda de um usuário logado. E lá se vão os seus dez mil.

    O nome "falsificação de requisição entre sites" faz todo sentido: um site (o malicioso) forja uma requisição que parece ter vindo de você para outro site (o banco).

    #Qual o objetivo de se proteger contra isso?

    O objetivo da proteção CSRF é garantir que uma requisição que altera estado (um POST, PUT, PATCH ou DELETE) tenha realmente partido da sua aplicação, e não de um terceiro se aproveitando da sessão ativa do usuário.

    Repara que o problema não é autenticação. A vítima está autenticada de verdade, o cookie é legítimo. O problema é a intenção: como o servidor prova que foi o usuário quem, de fato, quis fazer aquela ação, e não um site externo agindo por baixo dos panos? É exatamente esse buraco que o token CSRF fecha.

    #Como o Laravel resolve isso

    A estratégia usada pelo Laravel (e pela maioria dos frameworks) é o padrão do token sincronizador. A lógica é a seguinte: para cada sessão de usuário, o Laravel gera um token secreto e aleatório. Esse token fica guardado na sessão, no lado do servidor.

    Toda vez que você gera um formulário, precisa embutir esse mesmo token dentro dele. É aí que entra a diretiva @csrf:

     1<form method="POST" action="/perfil">
     2    @csrf
     3    <input type="text" name="nome">
     4    <button>Salvar</button>
     5</form>
    

    Essa diretiva simplesmente expande para um campo escondido com o token:

     1<input type="hidden" name="_token" value="a-string-aleatoria-e-secreta-da-sessao">
    

    Quando o formulário é enviado, o middleware VerifyCsrfToken entra em ação. Ele compara o _token que veio no corpo da requisição com o token guardado na sessão. Se os dois baterem, a requisição segue. Se não baterem, ou se o token não vier, o Laravel barra tudo com aquela resposta 419.

    E por que isso funciona? Porque o site malicioso não tem como saber qual é o seu token. Ele está guardado na sua sessão e só é entregue nas páginas que o próprio banco renderiza pra você. Por conta da Same-Origin Policy, o site do atacante não consegue ler o conteúdo das páginas de banco.com para roubar o token. Sem o token correto, a requisição forjada morre no middleware.

    #Onde o token é verificado

    No Laravel moderno, o middleware VerifyCsrfToken já vem ativado por padrão no grupo web. Você não precisa configurar nada pra ele funcionar, ele simplesmente está lá protegendo suas rotas.

    Vale reforçar: a verificação só acontece em métodos que alteram estado (POST, PUT, PATCH, DELETE). Requisições GET não são verificadas, e nem deveriam ser, já que por convenção GET não muda nada no servidor. Essa é justamente a razão de nunca colocarmos ações destrutivas atrás de um simples link GET.

    #E quando é uma requisição via JavaScript?

    Nem sempre a gente usa formulário tradicional. Quando você faz uma chamada AJAX ou usa Axios, não tem um @csrf no HTML. Nesses casos, o Laravel também disponibiliza o token de outras formas.

    Uma opção clássica é colocar o token em uma meta tag no <head>:

     1<meta name="csrf-token" content="{{ csrf_token() }}">
    

    E então mandar ele no cabeçalho da requisição:

     1const token = document.querySelector('meta[name="csrf-token"]').content;
     2
     3fetch('/perfil', {
     4    method: 'POST',
     5    headers: {
     6        'X-CSRF-TOKEN': token,
     7        'Content-Type': 'application/json',
     8    },
     9    body: JSON.stringify({ nome: 'Lucien' }),
    10});
    

    O middleware sabe procurar o token tanto no campo _token do corpo quanto no cabeçalho X-CSRF-TOKEN. Se você usa Axios com o starter kit do Laravel, boa parte disso já vem configurada automaticamente, inclusive a leitura do cookie XSRF-TOKEN que o framework envia.

    #E as APIs? Precisam de token CSRF?

    Essa é uma dúvida comum. APIs stateless, autenticadas por token (tipo Sanctum com tokens de acesso ou JWT), não precisam de proteção CSRF. E o motivo é direto: CSRF só é um problema quando a autenticação depende de algo que o navegador envia sozinho, como o cookie de sessão.

    Quando a autenticação é feita por um header Authorization: Bearer <token>, o navegador não anexa esse cabeçalho automaticamente. O site malicioso teria que conhecer o token e adicioná-lo na mão, e se ele já tem o token, o CSRF é o menor dos seus problemas. Por isso as rotas do arquivo de API do Laravel ficam fora do middleware web e não passam pela verificação de token.

    O cuidado com CSRF vale principalmente para aplicações que usam sessão baseada em cookie, que é o caso da maioria das aplicações web tradicionais com Blade.

    #Resumindo

    CSRF é um ataque que abusa do fato de o navegador enviar seus cookies de sessão automaticamente, permitindo que um site malicioso forje requisições autenticadas em seu nome. A defesa é o token sincronizador: um segredo que só a sua aplicação conhece e que precisa acompanhar toda requisição que altera estado.

    No Laravel isso é praticamente transparente pra gente. Basta lembrar do @csrf nos formulários e mandar o token nos cabeçalhos das requisições via JavaScript, que o middleware VerifyCsrfToken cuida do resto. Da próxima vez que aparecer aquele 419 Page Expired, você já sabe: alguém esqueceu de mandar o token.

    profile image of Lucien Risso Coreia

    Lucien Risso Coreia

    Engenheiro de Software, entusiasta de tecnologia e criador de conteúdo.

    Mais posts de Lucien Risso Coreia