O Dia em que Criei uma Defesa CSRF

Introdução

Há algum tempo, passei por uma experiência que me ensinou muito sobre resiliência e a capacidade de solucionar problemas de forma criativa. Tudo começou quando um cliente que estava testando o sistema que eu estava desenvolvendo e reclamou que as requisições não possuíam proteção contra ataques CSRF (Cross-Site Request Forgery). Apesar de já ter visto referências a isso no framework Laravel, eu não tinha um conhecimento profundo sobre o assunto. No Laravel, a proteção contra CSRF é simples de implementar, mas e quando você precisa criar uma solução do zero, sem a utilização de framework? Mas antes de contar a minha história, vou explicar um pouco sobre o que é CSRF.

O que é CSRF?

CSRF (Cross-Site Request Forgery) é um tipo de ataque que explora a sessão autenticada de um usuário em um site para executar ações maliciosas sem o conhecimento do usuário. Imagine que você está autenticado no site A. Se você clicar em um link malicioso no site B, esse link pode fazer uma requisição não autorizada ao site A, utilizando sua sessão ativa. Esse tipo de ataque pode ter consequências graves, desde alterações indesejadas em configurações até roubo de dados.

Vou dar um exemplo de como este ataque pode acontecer:

Imagine que você está autenticado em um site de rede social, onde pode postar mensagens. O site tem um formulário que permite a postagem de mensagens, acessível através de uma URL como esta:

https://minharede.com/postar_mensagem

O atacante cria um site malicioso e insere um código HTML que faz uma requisição POST para essa URL:

<html>
<body>
  <h1>Veja este vídeo incrível!</h1>
  <button onclick="assistirVideo()">Assistir</button>
  <script>
    function assistirVideo() {
      let form = document.createElement('form');
      form.method = 'POST';
      form.action = 'https://minharede.com/postar_mensagem';

      let input1 = document.createElement('input');
      input1.type = 'hidden';
      input1.name = 'mensagem';
      input1.value = 'Eu fui hackeado!';
      form.appendChild(input1);

      document.body.appendChild(form);
      form.submit();
    }
  </script>
</body>
</html>

Quando você, o usuário autenticado, clica no botão para "assistir o vídeo", o navegador faz a requisição para o site de rede social usando suas credenciais autenticadas. Como resultado, o site de rede social processa a postagem da mensagem sem sua autorização ou conhecimento, publicando "Eu fui hackeado!" em seu perfil.

Esse é um exemplo clássico de ataque CSRF, onde a vulnerabilidade é explorada pela ausência de verificação adicional sobre a origem das requisições. É crucial implementar medidas de proteção contra CSRF, como tokens de verificação, para garantir que as requisições são legítimas e provenientes de fontes autorizadas.

A Descoberta do Problema

Durante os testes do sistema que estava entregando, fui surpreendido com uma informação do cliente que me deixou um pouco perplexo. O cliente me informou que nos testes ele percebeu que as requisições não estavam protegidas contra ataques CSRF. Meu primeiro impacto foi de surpresa e confusão, pois eu tinha visto algo sobre isso no Laravel, mas não sabia como funcionava realmente. No Laravel, a proteção era simples: bastava usar uma função específica e pronto, estava seguro.

Para contextualizar, no Laravel, a proteção contra CSRF é bastante direta. Você só precisa adicionar a diretiva @csrf no formulário. Aqui está um exemplo de como isso é feito:

<form method="POST" action="/route">
    @csrf
    <input type="text" name="field" value="value">
    <button type="submit">Submit</button>
</form>

Com isso, o Laravel automaticamente verifica a validade do token CSRF em cada requisição POST, PUT, PATCH ou DELETE, garantindo que a requisição seja legítima.

Porém, no meu caso, o sistema já estava quase completo e não seria viável recomeçar do zero usando Laravel. Foi então que decidi buscar uma solução personalizada. Sabia que precisava criar uma forma de proteger as requisições sem comprometer todo o trabalho já feito.

Criando uma Solução Personalizada

Para solucionar o problema, comecei pesquisando sobre o CSRF e como implementar uma defesa eficaz. Descobri que um ataque CSRF explora a sessão autenticada de um usuário para enviar requisições maliciosas sem seu conhecimento. O objetivo era evitar que uma pessoa mal-intencionada utilizasse requisições de outro site para aproveitar a sessão de um usuário no meu site.

Decidi criar um token que seria incluído em cada requisição AJAX enviada para o backend. Esse token seria então validado para garantir a autenticidade da requisição. Essa abordagem envolveu algumas ações específicas:

Passo a Passo da Solução

1. Gerando o Token

A ideia era criar um token usando algum dado do próprio usuário que estava com a sessão ativa no navegador, concatenado com o timestamp atual. Usamos uma função de criptografia desenvolvida por um amigo para garantir a segurança dos dados.

/**
 * Funções para gerar segurança CSRF
 */
function gerar_token() {
    if (empty($_SESSION['emaildousuario'])) {
        $emaildousuario = "";
    } else {
        $emaildousuario = $_SESSION['emaildousuario'];
    }
    $token = criptografar($emaildousuario . '_' . time());
    return $token;
}

2. Utilizando o Token

Essa função gerar_token era chamada em todas as páginas que faziam requisições para o backend. Em todas as requisições AJAX, enviava o token via método GET para o backend.

function algumaCoisaParaBackend(coisa) {
    const coisa = coisa;

    $.ajax({
        type: "POST",
        data: { coisa },
        url: "ajax_enviar_alguma_coisa.php?token=<?php echo $token; ?>",
        success: function(data) {
            var response = JSON.parse(data);
            if (response.status == 'success') {
                document.getElementById("footer-alert-confirm").style.display = "flex";
                setTimeout(function() {
                    document.getElementById("footer-alert-confirm").style.display = "none";
                }, 5000);
            } else {
                alert(response.info);
                console.log(data);
            }
        }
    });
}

3. Validando o Token

No backend, uma função check_token recebia o token como parâmetro e realizava a validação.

function check_token($token) {
    $tempo_token_ativo = time() - 24 * 60 * 60;
    $token_desprotegido = descriptografar($token);
    $token_array = explode('_', $token_desprotegido);

    if (count($token_array) > 1) {
        $token_time = $token_array[1];
        $email = $token_array[0];
    } else {
        $token_time = $token_array[0];
    }

    if ($email == "") {
        if ($token_time <= $tempo_token_ativo) {
            exit('Por questões de segurança, atualize a página e tente novamente.');
        }
    } else {
        if ($token_time <= $tempo_token_ativo && $email != $_SESSION['emaildousuario']) {
            exit('Por questões de segurança, atualize a página e tente novamente.');
        }
    }
}

Reflexões e Aprendizados

Você pode estar se perguntando por que não utilizar simplesmente um código qualquer criptografado e verificar no servidor com a função descriptografar. Essa seria uma solução plausível, especialmente porque a função de criptografia utilizada era interna e desenvolvida por um colega, o que tornava improvável que alguém conseguisse quebrar a criptografia para manipular o código.

Porém, optei por aproveitar a oportunidade para implementar uma camada adicional de segurança ao utilizar o time(). Isso não apenas validou o token, mas também limitou o tempo de validade da sessão do usuário, reforçando ainda mais a segurança do sistema. Embora existam outras formas de gerenciar o tempo da sessão, essa abordagem integrada ajudou a fortalecer as defesas do sistema contra possíveis vulnerabilidades.

Essa solução foi desenvolvida na época e resolveu o meu problema inicial. Refletindo sobre essa experiência, percebo a importância de ser resiliente e não fugir dos problemas, mas sim enfrentá-los e buscar soluções. Utilizei o "Princípio da Oportunidade" para ajustar várias camadas ao mesmo tempo, aumentando a segurança do sistema.

Embora frameworks como Laravel simplifiquem a implementação de segurança, é possível desenvolver soluções eficazes mesmo sem eles. Isso me fez enxergar que podemos criar soluções robustas adaptando-nos às necessidades específicas do projeto.

Conclusão

Resumindo, ao enfrentar desafios técnicos complexos, como a implementação de segurança contra CSRF, a chave está em aproveitar oportunidades para aprender e melhorar continuamente. Isso não só fortalece suas habilidades técnicas, mas também contribui para a segurança e confiabilidade dos sistemas que você desenvolve.

A experiência de desenvolver uma solução personalizada para proteção contra CSRF sem conhecimento prévio foi um dos maiores desafios que já enfrentei. Esse processo me ensinou a valorizar a resiliência e a habilidade de resolver problemas técnicos de maneira criativa. Se você está lidando com um problema semelhante, saiba que, mesmo sem utilizar ferramentas populares como o Laravel, é possível encontrar soluções eficazes adaptando-se às necessidades específicas do seu projeto.

Refletindo sobre essa experiência, percebo o quanto é crucial não apenas identificar problemas, mas também buscar soluções que possam ser implementadas com os recursos disponíveis. A solução que desenvolvi inicialmente foi eficaz para o momento, mas será que poderia ser melhor? Qual seria a sua opinião sobre a abordagem que escolhi? Você acredita que outras camadas de segurança poderiam ser adicionadas para tornar o sistema ainda mais robusto?

Estou interessado em ouvir sua opinião sobre este tipo de abordagem. Como você lidaria com um problema semelhante em seus projetos?


Gostou de aprender sobre como enfrentei o desafio de implementar segurança contra CSRF de forma personalizada? Quer receber mais dicas e insights sobre desenvolvimento e segurança de sistemas? Assine minha newsletter para ficar por dentro das últimas novidades e compartilhe suas experiências no LinkedIn!

Assine a Newsletter É de gratis 😉 | Me siga no LinkedIn