Autenticação Segura de Webhooks em Node.js com Validação HMAC e Módulo Crypto
Receber payloads de serviços externos como GitHub ou Stripe sem validar a origem expõe sua API a ataques de falsificação de requisições e injeção de dados maliciosos. O uso do módulo nativo crypto do Node.js resolve esse problema ao gerar uma assinatura HMAC SHA-256 no backend e compará-la com o cabeçalho recebido usando verificação em tempo constante.
JavaScript
const crypto = require('crypto');
/**
* Valida a assinatura HMAC de uma requisição de Webhook recebida.
* @param {string|Buffer} payloadBruto - O corpo bruto (raw body) da requisição.
* @param {string} assinaturaHeader - A assinatura enviada pelo serviço no cabeçalho.
* @param {string} segredoWebhook - A chave secreta compartilhada.
* @returns {boolean} True se a assinatura for válida, false caso contrário.
*/
function validarAssinaturaWebhook(payloadBruto, assinaturaHeader, segredoWebhook) {
// 1. Gera o hash HMAC SHA-256 usando o segredo e o payload bruto
const hmacCalculado = crypto
.createHmac('sha256', segredoWebhook)
.update(payloadBruto)
.digest('hex');
// Ajusta o formato caso o serviço envie um prefixo (ex: 'sha256=...')
const assinaturaEsperada = assinaturaHeader.replace(/^sha256=/, '');
// Converte para Buffers de tamanho equivalente para evitar Timing Attacks
const bufferCalculado = Buffer.from(hmacCalculado, 'utf8');
const bufferRecebido = Buffer.from(assinaturaEsperada, 'utf8');
if (bufferCalculado.length !== bufferRecebido.length) {
return false;
}
// 2. Compara as assinaturas em tempo constante seguro
return crypto.timingSafeEqual(bufferCalculado, bufferRecebido);
}
// --- Exemplo Prático de Uso ---
const segredoConfigurado = 'chave_secreta_super_segura_123';
const payloadExemplo = JSON.stringify({ evento: 'payment_intent.succeeded', valor: 15000 });
// Simulação da assinatura gerada pelo provedor externo (ex: GitHub/Stripe)
const assinaturaValidaSimulada = 'sha256=' + crypto
.createHmac('sha256', segredoConfigurado)
.update(payloadExemplo)
.digest('hex');
const eValido = validarAssinaturaWebhook(payloadExemplo, assinaturaValidaSimulada, segredoConfigurado);
if (eValido) {
console.log('[Sucesso] Webhook autêntico! Processando evento...');
} else {
console.error('[Erro] Assinatura inválida! Requisição descartada.');
}
Pontos-chave
- Proteção Contra Timing Attacks: O uso de
crypto.timingSafeEqual()previne que invasores descubram o hash inspecionando variações no tempo de resposta da comparação padrão de strings (===). - Uso Obrigatório do Raw Body: A verificação deve utilizar o corpo bruto não parseado da requisição (
Bufferou string original); executarJSON.parse()antes do HMAC altera o espaçamento e invalida a verificação. - Módulo Nativo do Node.js: Não requer pacotes externos de terceiros, garantindo excelente performance e eliminando vulnerabilidades na árvore de dependências.
- Formatos de Cabeçalhos: Serviços populares enviam a assinatura em formatos específicos (ex:
x-hub-signature-256no GitHub oustripe-signatureno Stripe), exigindo o tratamento do prefixo antes da comparação.
