On a go-kart reservation platform I built, Mercado Pago sends a webhook every time a payment's status changes. The handler updates the reservation, sends a confirmation, and — this is the part that matters — releases the slot back to the pool if the payment failed.
The signature verification looked correct: read the x-signature header, hash the payload with the webhook secret, compare. Standard stuff.
The assumption that broke
The bug wasn't in the hashing. It was in what got hashed. Mercado Pago signs a template string built from specific fields in the request — not the raw body. My handler was hashing JSON.stringify(req.body) and comparing that against a signature computed over the documented template.
Since the two hash inputs almost never matched anyway for legitimate requests either, the check had silently degraded into "does a signature header exist," which anyone could produce.
// before — hashes the wrong thing, always "works" for anyone
function verify(req: Request, secret: string) {
const sig = req.headers.get("x-signature");
const hash = hmacSha256(JSON.stringify(req.body), secret);
return sig?.includes(hash);
}
// after — build the exact template MP signs, per their docs
function verify(req: Request, secret: string, ts: string, id: string) {
const template = `id:${id};request-id:${req.headers.get("x-request-id")};ts:${ts};`;
const hash = hmacSha256(template, secret);
const sig = parseSignatureHeader(req.headers.get("x-signature"));
return timingSafeEqual(hash, sig.v1);
}
Two more things went in alongside the fix: a timingSafeEqual comparison instead of includes (timing attacks on string comparison are a real, if slow, way to leak a hash byte by byte), and idempotency on the reservation update, keyed on the payment ID — because once you're taking webhooks seriously, you also have to accept they'll arrive more than once.
Nothing here is exotic. It's the kind of bug that survives code review because the code looks like it verifies something, and only fails an audit that actually replays the provider's exact signing process.