Webhook Receiver Hardener
Installation
SKILL.md
Webhook Receiver Hardener
Turn an inbound webhook route into hostile-input-safe code: verify before you trust, persist before you ack, ack before you process.
A webhook endpoint is an unauthenticated, internet-facing write path that an attacker can spam and a flaky sender will hammer with retries. Treat every byte as untrusted until the signature checks out.
Workflow
- Capture the raw body. Read the exact request bytes before any JSON parse or body-parsing middleware runs. A re-encoded body changes whitespace and ordering and silently breaks the HMAC. If the framework buffers/reserializes by default, disable it for this route.
- Verify the signature. Compute the HMAC over the raw bytes with the signing secret loaded from config. Compare with a constant-time equality function - never
==or string compare. Support two active secrets so secret rotation does not drop traffic. Reject failures with 401 before doing any other work. - Reject replays by timestamp. Most providers sign a timestamp alongside the payload. Verify it is part of the signed material, then reject requests whose timestamp falls outside a tolerance window (commonly 5 minutes) to blunt captured-request replay.
- Persist the raw event, then enqueue, then ack. Synchronously do only the minimum: store the verified raw payload and its event ID durably, enqueue a background job, return 2xx. Return success only after the event is durably stored so a crash mid-processing causes safe redelivery rather than silent loss.
- Process asynchronously and order-tolerantly. Run all business logic in the background worker, not the request. Webhooks arrive out of order, so never assume sequence: act on the event's own version/timestamp, or refetch current state from the provider's API rather than mutating from a possibly-stale payload. For strict ordering, partition the queue by resource ID.
- Return the right status. 2xx tells the sender to stop retrying; non-2xx requests a retry. A malformed-but-authentic event goes to a dead-letter queue and is acked, not retried forever. Log event ID and type on every receipt.
Worked example
The classic broken handler versus the hardened one (Express + Stripe-style signatures):
Bad: