Segurança de webhooks
Qualquer pessoa pode fazer POST para uma URL pública, então verifique cada entrega
antes de agir com base nela. A EvoMap assina cada webhook com um HMAC cuja chave é o
secret de assinatura que você recebeu quando
registrou o endpoint. Uma requisição que falha na verificação deve
ser rejeitada.
O cabeçalho de assinatura
Cada entrega carrega:
X-EvoMap-Webhook-Signature: t=1718000000,v1=<hex-hmac>
t— o timestamp Unix de quando a assinatura foi criada.v1— HMAC-SHA256, codificado em hexadecimal, calculado sobre a string`${t}.${rawBody}`(o timestamp, um.literal e então o corpo bruto da requisição) usando seusecretde webhook como chave.
Um cabeçalho legado X-EvoMap-Signature: sha256=<hmac over body> (HMAC apenas sobre o
corpo, sem timestamp) também é enviado para compatibilidade retroativa. Prefira
X-EvoMap-Webhook-Signature — o esquema com timestamp é o que permite rejeitar
repetições.
Verifique uma entrega
Calcule o v1 esperado sobre `${t}.${rawBody}` e compare-o com o valor do
cabeçalho em tempo constante. Duas regras importam:
- Assine sobre os bytes brutos do corpo, exatamente como recebidos — verificar contra um objeto JSON reserializado vai falhar, porque a ordem das chaves e os espaços em branco diferem.
- Rejeite uma entrega cujo
testá fora da sua janela de tolerância (por exemplo, ±5 minutos) para se proteger de capturas repetidas.
import { createHmac, timingSafeEqual } from "node:crypto";
/**
* @param {string} rawBody - the exact request body bytes
* @param {string} header - value of X-EvoMap-Webhook-Signature
* @param {string} secret - your webhook signing secret (whsec_…)
* @param {number} toleranceSec
* @returns {boolean}
*/
export function verifyWebhook(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(
header.split(",").map((kv) => kv.split("=")),
);
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 || "");
return a.length === b.length && timingSafeEqual(a, b);
}
import hmac, hashlib, time
def verify_webhook(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool:
parts = dict(kv.split("=", 1) for kv in header.split(","))
t = int(parts.get("t", 0))
if not t or abs(time.time() - t) > tolerance:
return False
signed = f"{t}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
Checklist
- Leia primeiro o corpo bruto. Capture os bytes do corpo antes que qualquer parsing de JSON ou middleware de framework os reserialize.
- Compare em tempo constante (
timingSafeEqual/hmac.compare_digest) — nunca com==— para evitar canais laterais de temporização. - Aplique a janela de timestamp. Uma assinatura válida com um
tobsoleto é uma repetição; rejeite-a. - Retorne
2xxsomente após verificar. Em caso de falha na verificação, retorne4xxe não faça nada. - Mantenha o secret no servidor. Rotacione-o (exclua e registre o webhook de novo) se ele pode ter vazado.
Relacionado
- Webhooks — registro e o secret de assinatura de uso único
- Catálogo de eventos — o envelope que você está verificando
- Entrega e novas tentativas — o que uma entrega rejeitada dispara