Application Security

An Unverified Webhook Endpoint Accepts Instructions From Anyone

Key takeaway: A webhook URL is reachable by the entire internet. Without signature verification it is an unauthenticated endpoint that performs privileged state changes on request.

The Failure Is Simple and Severe

A provider posts an event to your endpoint. Your handler parses the JSON, reads the order identifier and status, and marks the order paid.

An attacker who discovers the URL sends the same JSON. There is no authentication, so the handler proceeds. Orders are marked paid, subscriptions activated, credits granted.

The URL is not secret in any meaningful sense. It appears in provider dashboards, in configuration committed to repositories, in browser network logs during testing, and it is guessable — /webhooks/stripe is not a difficult path to try.

Verifying Correctly

Providers sign each request with a shared secret, typically as an HMAC over the raw body plus a timestamp, delivered in a header.

Three details are commonly got wrong.

Verify against the raw body, not the parsed object. Re-serialising JSON changes byte order, whitespace and number formatting, so the recomputed signature will not match. Capture the raw bytes before any parsing middleware touches them.

Compare in constant time. A standard string comparison returns early on the first differing byte, which leaks information about the correct value through timing. Use the constant-time comparison function your language provides.

Check the timestamp. A signature stays valid forever unless the signed payload includes a timestamp that you verify is recent. Without that check, a captured request can be replayed indefinitely.

Control Prevents
Signature verification Forged events entirely
Raw body used for HMAC Verification failing spuriously
Constant-time comparison Timing-based signature discovery
Timestamp freshness window Replay of captured requests
Event ID deduplication Duplicate processing

Idempotency Is Not Optional

Providers retry on any non-2xx response, and they sometimes deliver the same event more than once even on success. A handler that grants credit without deduplication will grant it repeatedly.

Store processed event identifiers and skip anything already seen. The window need only cover the provider’s retry period, so the table stays small.

This also makes your handler safe to retry deliberately, which matters when a downstream dependency was briefly unavailable and you want to reprocess.

Handler Design

Respond quickly and process asynchronously. A handler that performs slow work inline exceeds the provider’s timeout, which triggers a retry, which starts the work again in parallel with the first attempt. Acknowledge receipt, enqueue, and do the work in a worker.

Never trust amounts or state from the payload alone for anything financially significant. Treat the webhook as a notification that something happened, then query the provider’s API for the authoritative record. That protects against both forgery you failed to catch and provider-side inconsistency.

Log every received event with its identifier and verification result, because webhook problems are invisible without that record and reconciliation depends on it.

The Bottom Line

Verify the signature over the raw body with a constant-time comparison, enforce a timestamp window, and deduplicate by event identifier. Respond immediately and process asynchronously, and confirm financially significant details against the provider’s API rather than trusting the payload.

Related Articles

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top button