A webhook arrives with a valid HMAC over its raw bytes. Middleware parses the JSON, changes whitespace and key order, then asks the verifier to authenticate the new serialization. The object is equivalent; the signature is not.
Preserve the signed input
import crypto from "node:crypto";
export function verify(raw, header, secret) {
const [ts, sig] = header.split(",");
if (Math.abs(Date.now()/1000-Number(ts)) > 300) return false;
const expected=crypto.createHmac("sha256",secret).update(`${ts}.`).update(raw).digest("hex");
return sig.length===expected.length && crypto.timingSafeEqual(Buffer.from(sig),Buffer.from(expected));
}
Enter fullscreen mode Exit fullscreen mode
Capture raw before JSON parsing. Bind the timestamp into the signature and store an event ID or signature digest in a five-minute replay cache. Reject a second delivery after the first succeeds.
Fixtures
Fixture Expected exact raw body accept once reordered keys reject one-byte mutation reject six-minute-old timestamp reject duplicate valid request reject as replayCreate one valid fixture with a fixed timestamp and secret, then derive every negative case from it. Never log the production signature or secret. Record only fixture ID, reason class, and request digest.
prevent: verify raw bytes before business logic
detect: reason-coded rejects and replay-cache hits
recover: rotate secret, retain sanitized evidence, replay known-good events deliberately
Enter fullscreen mode Exit fullscreen mode
This example covers HMAC webhooks, not asymmetric signatures or every provider header format. Follow the provider’s canonicalization rules. Which middleware in your stack first destroys access to the original bytes?
답글 남기기