HMAC — Como webhooks sabem que você não está mentindo
Qualquer servidor pode enviar uma requisição POST para seu endpoint de webhook. Assinaturas HMAC são como os enviadores legítimos provam que escreveram o payload — e como você verifica isso.
A cada vez que o Stripe cobra um cartão, dispara um webhook. A cada vez que o GitHub mergeia uma solicitação, dispara um webhook. Essas plataformas estão enviando solicitações POST para uma URL que você lhes deu — mas aqui está a verdade incômoda: qualquer outra pessoa pode enviar uma solicitação POST para a mesma URL.
Então, como o seu servidor sabe que a solicitação realmente veio do Stripe e não de um atacante que adivinhou a URL do seu ponto final? A resposta é HMAC — e uma vez que você entender, verá por que é a abordagem padrão em todas as plataformas de API sérias.
O Problema: Qualquer Pessoas Pode POSTAR NO SEU PONTO FINAL
Pontos de entrada de webhook são apenas URLs. Eles são publicamente acessíveis (é necessário, para que o remetente os alcance) e aceitam solicitações POST. Não há nada que impeça um atacante de criar um payload falso e enviá-lo para o seu ponto final.
Imaginem que seu manipulador de webhook faz isso quando recebe um evento:
if event["type"] == "payment.completed":
fulfill_order(event["data"]["order_id"])
Um atacante que conhece a URL do seu ponto final pode enviar um evento falso com qualquer ID de pedido que goste. payment.completed Sem verificação, seu servidor aceitaria pedidos que nunca foram pagos.
Você precisa de um jeito de verificar que o payload foi escrito por alguém que possui um segredo que vocês dois compartilham — sem transmitir esse segredo na solicitação.
O que é HMAC?
HMAC significa Código de Autenticação de Mensagem Baseado em Hash. É uma construção que combina uma função de hash criptográfica (normalmente SHA-256) com uma chave secreta para produzir um sinal. O sinal prova duas coisas:
- Autenticidade — o mensagem foi criada por alguém que possui a chave secreta
- Integridade — a mensagem não foi modificada durante a transmissão
A propriedade chave do HMAC: você não pode produzir um sinal válido sem conhecer a chave secreta. E você não pode reverter o sinal para recuperar a chave. É uma prova de uma única direção.
HMAC versus um hash simples
Um hash simples (como SHA-256) do payload resolve o problema de integridade, mas não da autenticidade. Um atacante que intercepta um payload válido poderia recalculá-lo para um payload modificado. O HMAC mistura a chave secreta em cada etapa do processo de hashing, então, sem a chave, você não pode produzir um sinal correspondente mesmo sabendo exatamente o algoritmo de hash sendo usado.
Como o HMAC de Webhook Funciona
O fluxo tem três etapas: troca de chave, assinatura e verificação.
Etapa 1: Troca de chave (ocorre uma vez)
Quando você configura um webhook com uma plataforma como Stripe ou GitHub, elas geram um segredo de webhook e o mostram para você uma vez. Você armazena no lado do servidor (nunca em código do lado do cliente ou repositórios públicos). Isso é tudo — a chave nunca viaja novamente pela rede.
Etapa 2: O remetente assina o payload
Antes de enviar o webhook, a plataforma calcula uma assinatura HMAC sobre o corpo bruto da solicitação usando a chave compartilhada:
signature = HMAC-SHA256(secret_key, request_body)
A assinatura é então anexada à solicitação, normalmente em um cabeçalho como X-Hub-Signature-256 (GitHub) ou Stripe-Signature (Stripe). O payload bruto viaja no corpo sem alterações.
Etapa 3: Você verifica na recepção
Quando seu servidor recebe o webhook, você recalcula o mesmo HMAC usando o corpo bruto e a chave armazenada, depois compara o resultado com a assinatura no cabeçalho. Se eles coincidem, o payload é autêntico e não foi modificado. Se não coincidem, rejeite.
expected = HMAC-SHA256(your_secret, raw_body)
if not constant_time_equal(expected, header_signature):
return 401
Perceber comparação de tempo constante — voltaremos a explicar por que isso importa.
Exemplos do Mundo Real
Stripe
Stripe envia um Stripe-Signature cabeçalho contendo um timestamp e uma ou mais assinaturas:
Stripe-Signature: t=1679000000,v1=abc123...,v0=oldformat...
A assinatura é calculada sobre timestamp.payload (concatenada com um ponto). Incluir o timestamp permite que o Stripe defenda contra ataques de replay — se um atacante capturar uma solicitação válida e a reenviar mais tarde, você pode rejeitá-la porque o timestamp é muito antigo.
GitHub
O GitHub envia um X-Hub-Signature-256 cabeçalho no formato sha256=<hex_digest>. A assinatura é HMAC-SHA256 do corpo bruto usando a chave de webhook que você configurou nas configurações do seu repositório.
Shopify
O Shopify usa um X-Shopify-Hmac-Sha256 cabeçalho com uma assinatura codificada em Base64 de HMAC-SHA256 — conceito igual, codificação diferente.
Verificação no Código
Aqui está como a verificação parece em três linguagens comuns. O padrão é idêntico — apenas as chamadas de biblioteca diferem.
Pitão
import hmac
import hashlib
def verify_webhook(secret: str, payload: bytes, signature_header: str) -> bool:
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
received = signature_header.removeprefix("sha256=")
return hmac.compare_digest(expected, received)
Node.js
const crypto = require('crypto');
function verifyWebhook(secret, rawBody, signatureHeader) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
const received = signatureHeader.replace('sha256=', '');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(received)
);
}
PHP
function verifyWebhook(string $secret, string $rawBody, string $signatureHeader): bool {
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
return hash_equals($expected, $signatureHeader);
}
Erros que quebram a segurança
1. Usar == em vez de comparação de tempo constante
Comparação padrão de strings (==, ===) curta-circuito assim que encontra uma diferença. Isso cria um canal de tempo: um atacante pode medir quanto tempo leva o seu servidor para rejeitar diferentes assinaturas. Strings que compartilham um prefixo mais longo levam um pouco mais tempo para serem rejeitadas. Com muitas requisições, um atacante pode usar isso para reconstruir um sinal válido byte por byte.
Sempre use comparação de tempo constante: hmac.compare_digest() em Python, crypto.timingSafeEqual() no Node.js, hash_equals() no PHP.
2. Parsear o corpo antes de verificar
O HMAC é calculado sobre os bytes brutos do corpo da solicitação. Se você parsear o JSON primeiro e depois re-serializar para verificar, você pode obter uma sequência de bytes diferente (ordem de chave diferente, espaços em branco, codificação). Sempre capture o corpo bruto antes que o parser do seu framework o toque, depois verifique com base nisso.
3. Não verificar ataques de replay
Uma solicitação assinada válida é válida para sempre — a menos que você verifique o timestamp. Se uma plataforma inclui um timestamp em seu esquema de assinatura (Stripe faz; GitHub não), rejeite solicitações onde o timestamp é mais antigo do que alguns minutos. Isso previne que um atacante capture e retransmita uma solicitação legítima.
4. Hardcode a chave no código-fonte
As chaves de webhook devem estar em variáveis de ambiente ou em um gerenciador de segredos, nunca commitadas no controle de versão. Uma chave vazada significa que um atacante pode falsificar qualquer payload para sempre — até que você a roteie.
O que o HMAC Não Protege
O HMAC prova que o payload foi assinado por alguém que possui a chave. Ele não não protege contra:
- Um remetente comprometido — se a infraestrutura de assinatura do Stripe fosse comprometida, eventos falsos ainda teriam assinaturas válidas
- Ataques de replay — a menos que você também valide um timestamp ou um nonce
- Confidentialidade — o HMAC não criptografa nada; o payload viaja em texto claro (embora HTTPS trate isso)
Para a maioria das integrações de webhook, o HMAC sobre HTTPS cobre tudo o que você precisa.
Instale nossas extensões
Adicione ferramentas de IO ao seu navegador favorito para acesso instantâneo e pesquisa mais rápida
恵 O placar chegou!
Placar é uma forma divertida de acompanhar seus jogos, todos os dados são armazenados em seu navegador. Mais recursos serão lançados em breve!
Ferramentas essenciais
Ver tudo Novas chegadas
Ver tudoAtualizar: Nosso ferramenta mais recente Texto (150 itens)
