Define what the signature actually protects
Webhook signatures commonly use a shared secret with HMAC-SHA256 to prove that the request bytes have not changed and that the sender possessed the secret. They do not encrypt the payload or prove that every field is sensible. After cryptographic validation, the application must still authorize the tenant, allow only expected event types, validate resource identifiers, and enforce valid business-state transitions. TLS remains mandatory because it protects the payload in transit and reduces the opportunity to capture a signed request for later replay.
Validate against the exact raw request body before JSON parsing. Parsing and serializing the object again can change whitespace, property order, Unicode representation, line endings, or numeric formatting. Construct the signed message exactly as the provider specifies, often from a timestamp, a delimiter, and the raw body bytes. Confirm that framework middleware, reverse proxies, and content decompression do not transform those bytes before verification.
- Algorithm policy:Accept only explicitly supported signature versions and algorithms; never let an untrusted header select arbitrary verification behavior.
- Digest comparison:Decode and validate the digest length first, then use a constant-time comparison function rather than ordinary string equality.
- Request limits:Enforce a reasonable body-size limit before buffering the entire payload, or signature checking itself can become a resource-exhaustion path.
- Error handling:Return consistent, minimal failures externally while retaining useful reasons in access-controlled internal telemetry.
A timestamp narrows the replay window but does not close it
An attacker does not need to break HMAC if they can resend a previously valid request. The sender should therefore include a timestamp inside the signed material, and the receiver should compare it with a trusted server clock. Choose the permitted skew from the provider's documented delivery latency and retry behavior. An overly narrow window rejects legitimate delayed events, while an overly generous one leaves captured requests reusable for longer.
The timestamp must be authenticated. Reading an unsigned date header provides no protection because an attacker can replace it while keeping the old body and signature. Keep application nodes synchronized and alert on clock drift so that separate instances do not disagree about validity. Reject clearly stale or future-dated events before expensive downstream work, but avoid response differences that reveal unnecessary details about which validation step failed.
Replay prevention also needs a unique value. Prefer a provider-issued event ID, delivery ID, or nonce that is covered by the signature. If none exists, derive a stable replay key from the signature version, signed timestamp, and a digest of the raw body. Store the key with an atomic create-if-absent operation in a database with a unique constraint or a suitable distributed store. Its retention must cover both the accepted clock window and plausible delayed retries. A separate read followed by a write is unsafe because simultaneous requests can both pass the initial check.
Separate transport retries from business idempotency
Most webhook delivery behaves like an at-least-once system. A timeout, broken connection, or unsuccessful response can cause the sender to retry a legitimate event. A duplicate is therefore not automatically malicious. An ingress replay key can suppress the same delivery, but business processing must remain idempotent because a provider may redeliver the same logical event under a new delivery ID or emit several events describing one resource.
A sound pipeline validates size, timestamp, and signature; atomically records the delivery; writes the verified event to a durable queue; and acknowledges it only after that write succeeds. Consumers then use a business-level key and source version when changing state. A payment handler might key an operation by transaction ID, while a resource synchronizer can retain the newest source revision. An in-memory set is insufficient because it disappears after restarts and cannot coordinate horizontally scaled instances.
- Duplicate delivery:Record the duplicate and return a safe success response so that the provider does not retry indefinitely.
- Out-of-order event:Ignore an older source version when a newer state has already been applied.
- Interrupted processing:Retry from the queue, with an idempotency key protecting every database write and external side effect.
- Permanent failure:Move the event to a quarantine or dead-letter path with enough context for investigation instead of retrying rapidly forever.
Operate secrets and verification as a lifecycle
Keep webhook secrets in a secrets manager or controlled runtime configuration, never in source code, browser-delivered settings, or general logs. During rotation, accept the current and immediately previous secret for a defined overlap period. If the provider supplies a key ID, resolve it through a strict allowlist rather than using arbitrary input to locate secrets. Remove the former key after the overlap and verify that every production and recovery environment received the new value. Separate secrets by provider, environment, and tenant where practical to limit the impact of exposure.
Telemetry should distinguish malformed headers, stale timestamps, unknown signature versions, signature mismatches, duplicate deliveries, queue-write failures, and downstream processing errors. Log an event identifier, provider, signature version, receipt time, and payload digest when useful, but not the secret, full signature, or sensitive body. Before release, test altered raw bytes, timestamp boundaries, concurrent duplicates, key rotation, clock drift, and queue outages. Secure webhook handling is not a single HMAC check; it is a fail-closed, retry-safe, auditable path from the network boundary to the final business side effect.