HMAC Como os Webhooks Sabem que Você Não Está Mentindo
A cada vez que o GitHub, o Stripe ou o Shopify envia um webhook para o seu servidor, assinam o payload com HMAC. Aqui está como isso funciona exatamente — e como verificá-lo no seu próprio código.
Você recebe um pedido POST alegando ser do Stripe. O payload afirma que um pagamento foi bem-sucedido. Você processa o pedido, envia os produtos — e três dias depois descobre que o pedido foi falso. Ufa.
Por isso, todos os provedores sérios de webhooks assinam seus payloads usando HMAC. Se você entende HMAC, entende por que o GitHub, o Stripe, o Shopify, o Twilio e praticamente todas as APIs modernas o usam — e saberá exatamente como verificar essas assinaturas no seu próprio código.
O que é HMAC na verdade
HMAC significa Código de Autenticação de Mensagem Baseado em Hash. Ele responde a uma pergunta: "O remetente que me enviou esta mensagem sabia o segredo compartilhado?"
Funciona misturando um hash criptográfico — geralmente SHA-256 — com uma chave secreta e o corpo da mensagem. O resultado é uma string de comprimento fixo que:
- muda completamente se um único byte da mensagem mudar
- não pode ser produzido sem conhecer a chave secreta
- não pode ser revertido para revelar a chave ou a mensagem original
A fórmula é compacta: HMAC(key, message) = H((key ⊕ opad) || H((key ⊕ ipad) || message)). Você não precisa memorizar os detalhes internos — todas as linguagens têm uma implementação padrão no pacote padrão — mas é útil saber o propósito: a chave é misturada no hash duas vezes, de maneiras diferentes, para impedir um tipo de ataque chamado de ataques de extensão de comprimento.
Como um provedor de webhook o usa
Quando você registra um ponto de entrada de webhook, o provedor fornece um segredo de assinatura — uma string aleatória que apenas você e eles conhecem. Quando um evento é disparado:
- O provedor serializa o payload do evento para JSON (ou uma string canônica).
- Ele calcula
HMAC-SHA256(secret, payload). - Ele envia o pedido com a assinatura no cabeçalho —
X-Hub-Signature-256para o GitHub,Stripe-Signaturepara o Stripe e assim por diante.
No seu lado, você faz o mesmo cálculo sobre o corpo original do pedido e o compara. Se coincidirem, o payload é autêntico. Se não, descarte-o.
Verificação no código
Aqui está como a verificação parece nas linguagens mais comuns. O padrão é idêntico em todas: calcular, comparar com uma função de comparação de tempo constante.
Node.js
const crypto = require('crypto');
function verifyWebhook(rawBody, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody) // rawBody must be a Buffer or string — NOT parsed JSON
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
Pitão
import hmac
import hashlib
def verify_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
raw_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
PHP
function verifyWebhook(string $rawBody, string $signature, string $secret): bool {
$expected = hash_hmac('sha256', $rawBody, $secret);
return hash_equals($expected, $signature);
}
O detalhe crítico: usar o corpo original
O erro mais comum na implementação é passar um payload parseado e re-serializado para a função HMAC em vez dos bytes brutos originais. A ordem das chaves, espaços em branco e escape de Unicode afetam o hash. O provedor assinou os bytes exatos que enviou pela rede — você deve hash exatamente esses mesmos bytes.
No Express (Node.js), isso significa configurar o parser de corpo para preservar o buffer bruto:
app.use('/webhooks', express.raw({ type: 'application/json' }));
No Django, use request.body em vez de request.data. No Flask, use request.get_data().
Por que não usar apenas um hash simples?
Um hash simples SHA-256 do payload prova nada — qualquer pessoa pode calcular SHA256(payload) sem um segredo. O segredo da chave é o que torna o HMAC um autenticação código, não apenas um checksum. Ele responde a "quem enviou isso" em vez de apenas "o conteúdo foi corrompido durante a transmissão".
Por que não usar assinaturas assimétricas (como RSA)?
O RSA e o ECDSA permitem que o receptor verifique a assinatura sem conhecer a chave privada — isso é valioso para divulgação pública (como assinatura de código). Para webhooks, há apenas dois lados que precisam verificar a assinatura: você e o provedor. Um segredo compartilhado é mais simples, mais rápido e igualmente seguro nesse modelo. Alguns provedores (Svix, Clerk) oferecem assinatura assimétrica de webhook para casos em que você não consegue armazenar segredos no servidor.
Ataques de replay — e como pará-los
Uma assinatura HMAC válida prova autenticidade, mas não frescor. Um atacante que captura um pedido legítimo assinado pode retransmiti-lo mais tarde. O Stripe combate isso incluindo um timestamp no cabeçalho e hashando o timestamp junto com o corpo do payload. No seu lado, você rejeita qualquer pedido onde o timestamp seja mais de cinco minutos antigo. Stripe-Signature Se você estiver construindo seu próprio sistema de webhook, faça o mesmo: inclua um nonce crescente ou um timestamp Unix no mensagem assinada e rejeite pedidos expirados no servidor.
A comparação de tempo seguro não é opcional
Nunca compare assinaturas HMAC com uma verificação simples (
). Uma comparação de string curta revela informações sobre quantos bytes iniciais coincidem — um atacante que faz milhares de requisições pode reconstruir a assinatura esperada byte a byte. Sempre use uma comparação de tempo constante:===, ==PHP:
- Node.js:
crypto.timingSafeEqual() - Python:
hmac.compare_digest() - Go:
hash_equals() - Ruby:
hmac.Equal() - Montando tudo: um manipulador de webhook pronto para produção
ActiveSupport::SecurityUtils.secure_compare()
Aqui está um exemplo completo usando o cabeçalho
do GitHub no Node.js: X-Hub-Signature-256 Referência rápida: quem usa o quê
const express = require('express');
const crypto = require('crypto');
const app = express();
const WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET;
// Keep the body as raw bytes — critical!
app.use('/github/webhook', express.raw({ type: 'application/json' }));
app.post('/github/webhook', (req, res) => {
const sigHeader = req.headers['x-hub-signature-256'];
if (!sigHeader) return res.status(401).send('Missing signature');
const sig = sigHeader.replace('sha256=', '');
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
// Safe to process the event now
console.log('Event type:', req.headers['x-github-event']);
res.sendStatus(200);
});
app.listen(3000);
Provedor
| Timestamp na assinatura? | Cabeçalho | Algoritmo | X-Hub-Signature-256 |
|---|---|---|---|
| GitHub | Stripe-Signature | HMAC-SHA256 | Não |
| Stripe | X-Shopify-Hmac-Sha256 | HMAC-SHA256 | Sim |
| Shopify | X-Twilio-Signature | HMAC-SHA256 | Não |
| Twilio | X-Slack-Signature | HMAC-SHA1 | Não |
| Folga | Paddle | HMAC-SHA256 | Sim |
| Paddle-Signature | HMAC: Como os Webhooks Sabem que Você Não Está Mentindo 2 | HMAC-SHA256 | Sim |
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 foi adicionado em 28 abr 2026
