Pular para o conteúdo
9 min de leitura

Segurança de webhooks: assinatura, verificação e proteção do endpoint

Compartilhar

Por Lucas Andrade ·

Webhook sem verificação de assinatura aceita qualquer payload forjado. Aprenda a assinar, validar HMAC, evitar replay e proteger o endpoint receptor.

Neste artigo

Webhooks são a espinha dorsal das integrações modernas. Em vez de sua aplicação ficar perguntando o tempo todo "aconteceu algo novo?", o serviço parceiro avisa você no momento em que o evento ocorre, enviando uma requisição para um endereço que você configurou. Um pagamento foi confirmado, um usuário se inscreveu, um arquivo terminou de processar: o webhook chega, e sua aplicação reage. É elegante e eficiente. O problema é que esse endereço que recebe as notificações é, por definição, público e aberto, e um endpoint público que dispara efeitos importantes é um convite para abuso.

Este texto trata da segurança do lado que recebe o webhook. O foco é como garantir que uma requisição que chega ao seu endpoint veio mesmo de quem diz ter vindo, que não foi adulterada, e que não é uma repetição maliciosa de uma mensagem legítima. Sem essas garantias, qualquer pessoa que descubra a URL do seu webhook pode forjar eventos e enganar a sua aplicação.

Por que o endpoint receptor é uma superfície de ataque

Pense no que um webhook costuma disparar: liberar acesso a um produto após um pagamento, marcar uma fatura como paga, provisionar um recurso, atualizar o estado de um pedido. São ações de consequência. Agora imagine que o seu endpoint aceita qualquer requisição bem formatada que chega nele. Um atacante que descobre ou adivinha a URL pode montar um corpo de mensagem imitando um evento de "pagamento confirmado" e enviá-lo. Se a sua aplicação confia cegamente, ela libera o produto sem que nenhum pagamento tenha ocorrido.

O ponto essencial é que a URL não é segredo suficiente. Ela vaza em logs, em históricos, em configurações, e mesmo que não vazasse, segurança por obscuridade não é segurança. Você precisa de uma forma criptográfica de provar que a mensagem veio do emissor legítimo e chegou intacta. É aí que entra a assinatura.

Assinatura HMAC: o mecanismo central

A técnica padrão para autenticar webhooks é a assinatura por HMAC. O emissor e o receptor compartilham um segredo, uma chave que só os dois conhecem. Ao enviar o webhook, o emissor calcula um código de autenticação sobre o corpo da mensagem usando esse segredo, uma operação que produz uma sequência de bytes que depende tanto do conteúdo exato quanto da chave. Esse código vai num cabeçalho da requisição. Ao receber, você recalcula o mesmo código sobre o corpo recebido, usando o mesmo segredo, e compara com o que veio no cabeçalho. Se baterem, a mensagem é autêntica e íntegra; se não baterem, você recusa.

A beleza do mecanismo é que ele resolve dois problemas de uma vez. Autenticidade, porque só quem tem o segredo consegue produzir um código válido, e o atacante não tem. Integridade, porque qualquer alteração no corpo, mesmo um único caractere, muda o código recalculado e denuncia a adulteração. O segredo compartilhado é o coração de tudo isso, e como todo segredo, ele nunca pode aparecer no cliente nem em código versionado; vive apenas no servidor, seguindo a mesma lógica de proteção de chaves que sustenta a criptografia na prática.

Compare em tempo constante

Um detalhe sutil e importante: ao comparar o código recebido com o recalculado, não use uma comparação comum de strings que para no primeiro caractere diferente. Esse tipo de comparação vaza informação pelo tempo que leva, e um atacante paciente pode explorar essas diferenças de tempo para descobrir o valor esperado byte a byte. Use uma comparação em tempo constante, que sempre percorre o valor inteiro e leva o mesmo tempo independentemente de onde está a diferença. As bibliotecas de criptografia oferecem essa função pronta; use-a.

Verifique sobre o corpo bruto

Outra armadilha frequente: a assinatura é calculada sobre os bytes exatos do corpo, então você precisa verificar sobre o corpo bruto recebido, e não sobre uma versão reconstruída. Se o seu framework lê o JSON, transforma em objeto e depois você re-serializa esse objeto para calcular o código, o resultado quase certamente não vai bater, porque a ordem das chaves, os espaços e a formatação podem diferir. Capture o corpo cru antes de qualquer parsing, calcule a assinatura sobre ele, e só depois processe o conteúdo. Muitos bugs de "a assinatura nunca bate" nascem exatamente daqui.

Impedindo replay de mensagens

Autenticar e verificar integridade ainda deixa uma brecha: um atacante que capture uma mensagem legítima e válida pode reenviá-la mais tarde. A assinatura continua correta, porque o conteúdo não mudou, mas a intenção é maliciosa, um replay. Se aquele webhook representava "cobrança bem-sucedida", reenviá-lo várias vezes poderia disparar a ação várias vezes.

A defesa contra replay tem duas peças que se complementam. A primeira é incluir um carimbo de tempo na mensagem, dentro do escopo da assinatura, e aceitar apenas mensagens cujo carimbo esteja dentro de uma janela de tolerância curta, alguns minutos. Uma mensagem capturada e reenviada horas depois cai fora da janela e é recusada. A segunda peça é a idempotência: cada evento carrega um identificador único, e você registra os identificadores já processados, ignorando qualquer repetição. Idempotência é valiosa também por outro motivo, que veremos a seguir, então implementá-la resolve dois problemas.

Idempotência e o comportamento de retry

Emissores de webhook costumam reenviar a mensagem se não receberem uma confirmação rápida, porque a rede falha e eles querem garantir entrega. Isso significa que a mesma notificação legítima pode chegar duas ou três vezes, sem malícia nenhuma. Se a sua aplicação processa cada chegada como um evento novo, você duplica ações: dois provisionamentos, duas liberações, dois registros. A idempotência, ancorada no identificador único do evento, garante que processar a mesma mensagem várias vezes tenha o mesmo efeito que processá-la uma vez. Guarde o identificador, cheque antes de agir, e trate a repetição como um sucesso silencioso.

Camadas extras de proteção

A assinatura HMAC resolve o problema central, mas defesa em profundidade recomenda camadas adicionais. Se o emissor publica a faixa de endereços de onde envia, você pode restringir o endpoint a aceitar requisições apenas dessas origens, embora isso não substitua a assinatura, apenas some a ela. Alguns provedores oferecem TLS mútuo, em que ambos os lados apresentam certificado, elevando a garantia de identidade. Exija sempre transporte cifrado, recusando requisições que não cheguem por conexão segura.

Trate o endpoint com o mesmo cuidado de qualquer entrada da sua API. Limite o tamanho do corpo aceito, para não permitir que payloads enormes causem negação de serviço. Aplique limites de taxa. Valide o conteúdo depois de autenticado, sem assumir que "veio assinado, logo é seguro em tudo"; a assinatura garante origem e integridade, não que os valores fazem sentido para o seu domínio. Essas preocupações são as mesmas que estruturam a segurança de APIs, e o endpoint de webhook é, afinal, uma rota de API que aceita entrada externa.

Responda rápido, processe depois

Uma boa prática operacional é validar a assinatura, registrar o evento e responder ao emissor imediatamente com uma confirmação, deixando o processamento pesado para acontecer de forma assíncrona. Isso evita timeouts que fariam o emissor reenviar, reduz a superfície de negação de serviço e desacopla a recepção do trabalho. A resposta rápida diz "recebi e vou cuidar disso", e o seu sistema cuida no seu ritmo, com idempotência protegendo contra as repetições que inevitavelmente virão.

Erros comuns de quem implementa

Vale catalogar as armadilhas em que mais gente cai ao implementar a verificação, porque conhecê-las de antemão poupa horas de depuração e, pior, evita deixar uma falsa sensação de segurança. A primeira, já mencionada mas central, é verificar sobre o corpo re-serializado em vez do corpo bruto. O desenvolvedor deixa o framework transformar o JSON em objeto, depois transforma o objeto de volta em texto para calcular a assinatura, e ela nunca bate, porque a ordem das chaves e a formatação mudaram. A frustração leva alguns a simplesmente desligar a verificação "porque não funciona", o que é o pior desfecho possível. Capturar o corpo cru antes do parsing resolve, e essa deve ser a primeira coisa a conferir quando a assinatura não confere.

A segunda armadilha é usar comparação comum de strings em vez de comparação em tempo constante, o que abre a porta para ataques de temporização que revelam a assinatura esperada aos poucos. É um erro silencioso, porque tudo funciona corretamente nos testes; a fraqueza só aparece sob um atacante paciente e instrumentado. A terceira é esquecer a proteção contra replay, verificando autenticidade e integridade mas aceitando alegremente que a mesma mensagem válida seja reenviada indefinidamente. A quarta é tratar a assinatura válida como um passe livre para confiar em todo o conteúdo, sem validar se os valores fazem sentido para o domínio: a assinatura prova origem e integridade, não que o pedido referenciado existe ou que o valor cobrado é coerente.

A quinta, mais operacional, é lidar mal com a rotação do segredo. Segredos de webhook precisam ser trocados de tempos em tempos, e uma implementação rígida que só conhece um segredo por vez causa uma janela de falha durante a troca. Bons receptores aceitam mais de um segredo simultaneamente por um período, permitindo a rotação sem perder mensagens. Antecipar isso no desenho evita uma correria futura. Cada um desses erros é sutil o suficiente para passar despercebido em código que "parece funcionar", e é por isso que revisar a implementação de webhook contra esta lista, item por item, vale o tempo investido.

Conclusão

Um webhook sem verificação é um endpoint público que executa ações importantes confiando em qualquer um que bater na porta, e isso é uma vulnerabilidade grave. A defesa correta se apoia na assinatura HMAC com segredo compartilhado guardado apenas no servidor, verificada em tempo constante sobre o corpo bruto da mensagem. Some a isso um carimbo de tempo com janela curta e idempotência por identificador de evento para barrar replay e lidar com os reenvios legítimos, e reforce com transporte cifrado, restrição de origem quando possível, limites de tamanho e taxa, e validação de conteúdo após autenticar. Com essas camadas, o seu endpoint deixa de aceitar eventos forjados e passa a confiar apenas no que consegue provar que é autêntico, íntegro e novo.

Compartilhar

Compartilhar

Leituras relacionadas

Nenhum comentário ainda

Seja o primeiro a comentar.

Deixe seu comentário

Entre com sua conta Canverly para comentar. Você pode usar a mesma conta em qualquer site da rede.

Entrar com Canverly