To review a webhook receiver, prove that it authenticates the exact received bytes before any event side effect, uses the right secret and provider-specific signature format, rejects invalid or stale deliveries where the protocol supports freshness, and produces one intended business effect across repeated deliveries. A public webhook endpoint is supposed to receive requests from the internet; a route's obscurity or an HTTPS connection does not prove who sent the request. A valid signature authenticates a message under a shared secret. It does not by itself authorize every action described inside that message, establish event ordering, or make a retried event new work.
OWASP's Webhook Security Cheat Sheet covers signature verification, replay controls and idempotent processing. The exact signing input and headers differ by sender: GitHub's delivery-validation guide documents X-Hub-Signature-256, while Stripe's webhook guidance requires the raw request body and its own endpoint secret for Stripe-Signature. Follow the sender's current specification, not a generic verifier copied from another integration.
Work through a payment-status receiver
Illustrative application and event IDs are invented. Larch Subscriptions receives a provider event saying an invoice was paid. The handler parses JSON, updates an account's entitlement, and returns success. A developer notices the code checks for X-Signature after json.loads and asks whether this is safe. Parsing before verification may be harmless if it is tightly bounded and has no side effects, but it can also mutate the exact bytes used for verification, consume resources, or cause logging and downstream behavior before authenticity is established. The review should inspect the full middleware chain and action order rather than the handler alone.
Use this illustrative verification flow, not a drop-in implementation for GitHub or Stripe:
Receive bounded raw request bytes.
Select the configured secret for this endpoint and sender.
Parse the sender's signature envelope without trusting event fields.
Compute the expected MAC over exactly the sender-defined signed bytes.
Compare signatures with a constant-time comparison; reject failures.
If the signed protocol has a timestamp, enforce its allowed age.
Parse and validate the event type, schema, account mapping, and object ID.
Atomically claim the provider event ID or delivery key for processing.
Guard the business effect by its own key and allowed state transition.
Durably capture pending work before acknowledging; record completion with the effect.
For synchronous processing, commit the idempotent effect before acknowledging.
The envelope and freshness step are provider-specific. Do not add a homemade timestamp to a signature scheme that does not authenticate one and assume it stops replay. Conversely, do not omit a documented freshness check merely because HMAC succeeds. Python's hmac.compare_digest provides a comparison intended to reduce timing leakage; the code must still parse, normalize and length-check the provider's signature representation correctly.
Show the bytes that the verifier receives
Two JSON bodies can describe the same object while containing different bytes:
received: {"id":"evt_17", "type":"invoice.paid"}
reserialized: {"id":"evt_17","type":"invoice.paid"}
The first includes a space after the comma; the second does not. These are synthetic strings with no accompanying signature. A protocol authenticating raw bytes does not grant the receiver permission to replace one with the other. Preserve the bounded raw body from ingress and give the provider's verifier the documented signature header, correct endpoint secret and exact signing input. Parse the authenticated event for business use afterward.
A verifier returning failure because middleware changed whitespace is usually an availability bug, not evidence that anyone forged a payment. The dangerous response would be to bypass verification when it fails. Inspect that exception branch and any “temporary” fallback accepting unsigned events.
The provider differences remain concrete. GitHub documents HMAC-SHA256 over the payload with X-Hub-Signature-256. Stripe uses its signature envelope and endpoint secret, and signs a timestamp for recency checks. Stripe also generates a new timestamp/signature for a retry, so a fresh signature does not make an old business event new work. Do not copy a timestamp rule into GitHub's signature input or treat an unauthenticated timestamp header as replay protection.
Separate durable receipt from completed business work
Larch's receiver verifies delivery D1 for event E17, then durably records it as pending. Only after durable capture does it acknowledge the sender under the chosen provider-compatible response policy. A worker applies the invoice transition. Consider three crash points:
| Crash point | Required recovery |
|---|---|
| Before durable capture | Provider retry can deliver the event again; nothing may falsely mark it complete |
| After capture, before business effect | Pending work remains claimable; duplicate delivery does not erase it |
| After business effect, before completion marker | Retry must discover the already-applied effect and finish without applying it twice |
A dedupe row called seen is not enough to distinguish these states. If the effect and event completion can share a transaction, commit them together. If an external side effect is involved, use its supported idempotency/reconciliation contract and retain the uncertain state until resolved. Do not claim cross-system exactly-once processing merely because one database has a unique event ID.
For this invoice-paid action, Larch binds provider environment/account, invoice ID and the allowed paid transition to the effect. Delivery E17 repeated, and a separately generated E18 for the same underlying paid occurrence, must leave one entitlement result. The object/event semantics determine the key: collapsing every future event for one subscription would incorrectly discard legitimate changes. The next section follows that account and state binding.
Trace the event to the local account
Larch may host many customer accounts behind one receiver. The signed event includes a provider object ID; the application must resolve that ID to an existing local account through a trusted binding. A client-supplied account ID in event metadata should not be treated as proof of ownership without checking how it was established. Look at the state transition: does “invoice paid” activate the intended subscription, or can it reactivate an already cancelled object? Does a later reversal or out-of-order event have a defined outcome? These are business-logic checks after authenticity.
Retries are normal. Stripe's duplicate-event guidance recommends recording processed event IDs, but also says two distinct Event objects can describe the same underlying occurrence; for those, it recommends checking data.object.id together with event.type. For Larch's invoice-paid action, claiming only the delivery or event ID would miss that second form. The reviewer should check a business-effect key tied to the invoice and paid transition, plus the local object's allowed state transition. An object ID and event type alone may be too broad for event types that legitimately recur for the same object, so choose the key from the provider's event semantics.
Claim the deduplication record atomically with the processing workflow or make recovery safe through an idempotent state change. A naive “check if ID exists, then insert” has a race between concurrent deliveries. The storage design depends on the application's transaction model, but the reviewer should demand an observed invariant: equivalent valid deliveries cannot grant the same entitlement twice or charge twice, even with different event IDs or after a timeout.
If a signature fails, log a server-created correlation ID and bounded reason category without logging the secret, complete body, private customer data or computed MAC. Any claimed provider event ID is untrusted until verification; it must not suppress later legitimate work. If a valid event cannot be processed, return or queue an outcome consistent with the sender's retry semantics. An acknowledgement sent before durable capture can lose work; an acknowledgement delayed until a long operation finishes can cause repeats. The handler's response strategy should be read alongside the sender's documented retry behavior.
Validate with the provider's own test facilities
Use an authorized test endpoint and the sender's official delivery or test tools. The developer and reviewer should record the sender environment, endpoint configuration, selected secret version and application revision, then check these cases:
- A genuine signed event produces exactly one expected state change.
- The same delivery repeated or concurrently delivered leaves one business result; a separate event ID for the same invoice-paid occurrence does too.
- A harmless change to the raw body is rejected before any side effect.
- A missing, malformed or wrong-endpoint signature is rejected.
- A signed event for an unknown or mismatched provider object does not update another account.
- If the provider signs a timestamp, an out-of-window delivery follows the documented rejection policy.
- Secret rotation accepts the intended overlap and then rejects the retired secret.
Do not put a live payment or production customer account at risk to demonstrate a forged delivery. A source-only inspection can identify the placement of checks and the storage rule, but it cannot establish that a particular gateway preserved the raw body or that retries are idempotent in deployment. Ask for configuration and observed test-delivery evidence where those facts matter.
The Larch finding note might say:
Scope: illustrative payment receiver at revision L12. Observed: signature verification uses a JSON string rebuilt after parsing; entitlement update occurs only after the helper returns. Risk: the rebuilt string may differ from the sender's signed bytes, causing rejection or tempting a bypass. Unknown: ingress raw-body availability and production helper configuration. Action: pass the documented raw bytes to the sender's verifier, then validate provider-object ownership, durably capture pending work and make the invoice-paid business effect idempotent across duplicate event IDs and crash recovery. Validation: provider test delivery plus repeat, altered-body and wrong-endpoint cases.
This note does not claim that an attacker forged a payment. It gives the developer a specific change and the reviewer a way to tell whether it worked.
Review endpoint configuration changes with the receiver code
Larch's secret selection must stay tied to the intended sender environment and endpoint. During migration, a developer may temporarily run old and new endpoints or accept overlapping secret versions. Record that overlap explicitly: which endpoint accepts which versions, when the old version is retired, and which evidence shows retirement at the receiver. A secret stored in the correct vault path is not proof that the deployed handler selected it.
Keep delivery authentication separate from local business mapping during the transition. A valid event from a test environment must not update a production entitlement merely because an object identifier resembles one in the local database. The receiver should resolve the provider environment/account and object binding that the application actually approved.
For a processing failure, preserve the authenticated event's durable state rather than treating every retry as a new payment. If the effect's status is uncertain after an external timeout, reconciliation needs an owner and a stable business-operation identity. The response policy should support that recovery without announcing completion before the work is durably captured.
A useful review handoff therefore has three owners: the endpoint owner establishes exact-byte verification and secret selection, the integration owner establishes provider-object mapping, and the business-service owner establishes the permitted idempotent transition. One cryptographic helper cannot prove all three. The final report should name which of those boundaries was inspected and which still requires selected-deployment or controlled-delivery evidence.
Next action
At one webhook endpoint, draw a line through the handler immediately before the first side effect. Everything needed to authenticate the raw delivery must occur before that line, and every business object referenced afterward needs an ownership check. For the wider question of repeated one-time actions, continue with replay protection review. For verifying a post-fix result, use the fix reassessment guide.