A webhook can confirm a payment, create an account, start a deployment, or unlock a door in another system. To the receiving application, it may look like an ordinary HTTP POST. Its effect is anything but ordinary.

There are two security boundaries to protect. The receiver must prove that an event is genuine. The sender must make sure an attacker cannot turn user-supplied callback URLs into a tunnel toward internal services.

TL;DR

  • HTTPS encrypts a webhook but does not prove that the request came from the expected publisher.
  • Verify the provider’s signature over the exact raw request body before parsing or processing it.
  • A valid signature does not stop replay. Enforce freshness and deduplicate authenticated event IDs where the protocol supports them.
  • Make handlers idempotent because legitimate providers retry deliveries.
  • If users can register callback URLs, defend the sending service against SSRF at registration and at connection time.

Why This Matters

Webhook endpoints are designed to be reachable by another service. They often cannot use a browser login, interactive MFA, or a normal CSRF token. Without a replacement control, anyone who finds the endpoint may be able to send events to it.

The consequences depend on the event. A forged payment.succeeded message could release goods. A replayed deployment event could run automation twice. A callback service that accepts https://127.0.0.1/ could be used to probe an internal admin interface.

The correct design treats every webhook as a small security protocol, not “just an API endpoint.”

Receiver Side: Prove Who Sent the Event

Verify the provider’s signature

Use the publisher’s maintained verification library when one exists. Different providers sign different fields and format their headers differently. Do not invent a “compatible” parser from a short example.

Verification usually follows this order:

  1. Read the exact raw request body as bytes.
  2. Extract the expected signature version, timestamp, and event identifier according to the provider’s protocol.
  3. Recalculate the HMAC or verify the asymmetric signature using the documented signed content.
  4. Compare MAC values with a constant-time function.
  5. Reject the request before JSON parsing or business processing if verification fails.

Re-serializing JSON before verification can change whitespace, field order, escaping, or encoding. The object may mean the same thing while the bytes no longer match the signature.

TLS is still required. A signature authenticates signed content; it does not hide personal or business data carried in the event.

Stop replay, not only forgery

A captured request can have a perfectly valid signature. If the protocol signs a delivery timestamp, reject events outside a small tolerance and keep systems synchronized. If it signs a unique event ID, record accepted IDs and prevent the same event from producing its effect twice.

Do not assume every provider signs timestamps and IDs. GitHub, Stripe, and other services have different formats and retry semantics. Apply the guarantee the actual protocol provides, then make the downstream action idempotent.

Expect legitimate duplicates

Publishers retry when a receiver times out or returns an error. The first attempt may have completed the database change even if the response never reached the sender.

A safe handler separates receipt from effect:

verify -> validate -> durably record or queue -> acknowledge -> process idempotently

Use the authenticated event ID, or a documented business idempotency key, to ensure that a payment, email, entitlement, or deployment is not applied twice. Return success for an already accepted valid duplicate without repeating the side effect.

A Minimal Verification Pattern

This pseudocode shows the decisions, not a provider-specific implementation:

raw_body = read_raw_body_with_size_limit()
signature = require_expected_signature_header()
timestamp = require_authenticated_timestamp()
if abs(now - timestamp) > allowed_skew:
reject(401)
expected = hmac_sha256(secret, timestamp + "." + raw_body)
if not constant_time_equal(expected, signature):
reject(401)
event = parse_and_validate_schema(raw_body)
if event_id_already_processed(event.id):
return 200
durably_queue(event)
return 200

Use this only as a mental model. Follow the provider’s exact canonicalization, header format, supported algorithms, key rotation, and error-handling guidance.

Keep webhook secrets in a secret manager, use a distinct secret per endpoint where supported, redact signature headers from logs, and rotate immediately after suspected exposure. During planned rotation, support the provider’s documented overlap process.

Sender Side: Callback URLs Create an SSRF Boundary

A webhook publisher often lets a customer choose the destination. That makes the delivery service an HTTP client controlled partly by untrusted input.

An attacker may register destinations such as:

  • loopback addresses;
  • private IPv4 or IPv6 ranges;
  • link-local and cloud metadata addresses;
  • internal hostnames;
  • a public hostname that later resolves to a private address;
  • a public URL that redirects to an internal destination.

This is server-side request forgery. A simple string blocklist is not enough.

Require https://, parse the URL with a trusted library, resolve every address, and reject non-public destinations. Repeat the check at connection time to reduce DNS rebinding risk. Control redirects or disable them; if they are allowed, validate every hop. Ensure the connection is made to the validated address while preserving the correct hostname for TLS verification. Route delivery workers through restricted egress so a validation mistake does not grant access to metadata services or internal networks.

Validate the Event After Authentication

A good signature proves origin and integrity. It does not prove that every field is safe for every downstream context.

After verification:

  • accept only expected event types and content types;
  • enforce a small body-size limit;
  • validate the payload against a strict schema;
  • use parameterized queries for database operations;
  • encode untrusted values when rendered into HTML;
  • authorize the event for the correct tenant and object;
  • queue expensive work and apply rate limits.

Do not log the full payload by default. Webhooks often contain customer, payment, repository, or account data. Log an event ID, type, endpoint identifier, result, latency, and a non-secret correlation value.

If a Forged or Replayed Webhook Was Processed

First stop new harmful events without destroying the evidence you need. If the signing secret may be exposed, disable the affected endpoint or restrict its actions, preserve the relevant application and delivery logs, and replace the secret through the provider’s documented emergency-rotation process. A planned overlap is useful during routine rotation; it is unsafe when the old secret is known to be compromised.

Then determine what the application actually did. Search by endpoint, event type, event ID, source context, and processing time. Compare received events with the publisher’s authoritative delivery history where one exists. Do not assume every duplicate is hostile: providers retry legitimate deliveries, and an application timeout can hide a successful first attempt.

Prioritize business effects over HTTP request counts:

  1. Identify payments, deployments, account changes, messages, or data transfers triggered by suspicious events.
  2. Pause or reverse those effects where the business process allows it. Involve the appropriate payment, platform, identity, or legal owner rather than attempting an undocumented rollback.
  3. Mark affected event IDs so retries cannot repeat the action, and fix the handler before re-enabling normal delivery.
  4. Check whether the same secret was reused by another endpoint or environment. Rotate every exposed credential, but avoid a broad rotation that breaks unrelated integrations without evidence.
  5. Monitor rejected signatures, duplicates, and downstream actions after recovery to confirm that the old path no longer works.

If the incident involved a sender-side SSRF weakness, isolate the delivery workers from internal and metadata networks, review their outbound connection logs, and determine what the service could reach or retrieve. Fix URL validation and egress controls before restoring user-controlled destinations. Rotating a webhook signing secret does not remediate SSRF.

How to Test the Design

In an authorized test environment, verify that:

  1. a missing, malformed, wrong-version, or incorrect signature receives a generic rejection;
  2. a stale signed request is rejected when the protocol supports authenticated timestamps;
  3. the same authenticated event produces its business effect once;
  4. an oversized or schema-invalid payload is rejected before processing;
  5. secret rotation accepts the intended active keys and rejects a revoked key;
  6. callback registration and delivery block loopback, private, link-local, metadata, redirect, and DNS-rebinding cases;
  7. a temporary downstream failure enters a retry or dead-letter process without silently losing the event.

Use the provider’s official testing tool where available. Never point webhook SSRF tests at networks you do not own or have permission to assess.

What You Can Do Today

  • Inventory webhook endpoints and identify which ones can cause money movement, access changes, or deployments.
  • Confirm that each receiver verifies the raw body with the provider’s supported library before parsing.
  • Send one legitimate event twice in staging and verify that its side effect occurs once.
  • Review whether full request bodies or signature headers are entering application logs.
  • If your product sends webhooks, test callback validation against IPv4, IPv6, redirects, and DNS changes—not only localhost.

A secure webhook needs authenticity, freshness, idempotency, input validation, and controlled egress. Missing any one of them leaves a different route to the same business impact.

Sources