Webhook Security for Web Apps: Signatures, Replay Protection, Idempotency, and SSRF

Secure webhook receivers and senders with signature verification, replay protection, idempotency, safe asynchronous processing, and SSRF controls.

Back to Blog

Webhook Security for Web Apps: Signatures, Replay Protection, Idempotency, and SSRF

Webhooks look simple because the transport is simple: one application sends an HTTP request to another when something happens. The security model is not simple. The receiving application has to decide whether the event is authentic, whether it is fresh, whether it has already been processed, and whether processing it can be abused to reach other systems.

A webhook endpoint for payments, source control, CRM updates, or deployment events is part of the application's attack surface. Treating it like an ordinary unauthenticated form endpoint is a mistake. A robust design verifies signatures over the original request body, rejects stale replays, processes event IDs idempotently, limits the work done before acknowledgment, and constrains any outbound URLs the event can influence.

Threat-model the webhook before writing code

Start with what the endpoint can cause. A payment event might mark an invoice as paid. A source-control event might trigger a deployment. A CRM event might create an account. A support integration might fetch an attachment from a URL supplied in the payload.

Those actions create different risks:

  • forged events that trigger business actions;
  • replayed legitimate events that repeat an action;
  • duplicate delivery caused by normal provider retries;
  • large or malformed payloads that consume resources;
  • callback or attachment URLs that cause server-side request forgery;
  • secret leakage through logs;
  • slow processing that causes retry storms.

The OWASP Webhook Security Cheat Sheet treats webhook security as both receiver and sender security. Most application teams first need to get the receiver side right.

Verify the signature over the raw body

Webhook providers commonly sign the event with an HMAC or asymmetric signature. The critical implementation detail is that the signature is usually computed over the exact bytes transmitted, sometimes combined with a timestamp or event identifier. Parsing JSON and then serializing it again can change whitespace, key order, or escaping and cause valid signatures to fail.

A generic HMAC verification flow looks like this:

rawBody = readRequestBytes()
timestamp = requestHeader("X-Webhook-Timestamp")
provided = requestHeader("X-Webhook-Signature")

signed = timestamp + "." + rawBody
expected = HMAC_SHA256(webhookSecret, signed)

if !constantTimeEqual(expected, provided):
    rejectRequest()

The header names and signed format differ by provider. Follow the provider's documented construction exactly. Do not invent your own parser around a third-party signature format.

Use constant-time comparison

When comparing MACs or signatures represented as bytes, use the language or framework's constant-time comparison function. Ordinary string comparison may reveal timing differences that are unnecessary in an authentication path.

Also validate the encoding and expected length before comparison. A malformed signature should fail cleanly without passing unusual values into cryptographic helpers.

Keep signing secrets out of code and logs

Webhook secrets belong in the same category as other application credentials. Store them in a secret-management mechanism or protected environment configuration, not in a repository. Rotate them when staff, vendors, or infrastructure changes require it.

Never log the secret. Be careful with debug logging around headers because a generic "log all request headers" middleware can expose authentication material. Redact signature headers and authorization tokens before centralized logs receive them.

A valid signature can still be a replay

If an attacker obtains a previously valid signed request, the signature may still verify. That is why freshness and uniqueness checks are separate from signature verification.

Use the timestamp mechanism documented by the provider. Reject events outside a small, justified acceptance window. Then record a unique event ID or delivery ID so the same valid event cannot perform the business action twice.

if abs(now - eventTimestamp) > allowedSkew:
    rejectAsStale()

if eventStore.has(eventId):
    return alreadyProcessed()

eventStore.reserve(eventId)
enqueue(event)
return accepted()

The exact sequence needs to be atomic enough for your workload. Two concurrent deliveries of the same event should not both pass a simple "does it exist?" query before either creates the record.

Idempotency is also reliability

Webhook providers retry. Networks fail. Your endpoint can return a timeout even though the business action finished. Duplicate delivery is therefore normal behavior, not necessarily an attack.

Design the handler so processing the same event twice has the same final outcome as processing it once. Use the provider's event ID where it is guaranteed to be unique, or define an application-level idempotency key. Store the processing state and result where retries can see it.

A payment event should not create two credits because it arrived twice. A user-created event should not send two welcome packages because the first acknowledgment was lost.

Acknowledge quickly and process asynchronously

OWASP recommends fast acknowledgment and asynchronous processing for substantial webhook work. The request path should verify the event, perform basic validation, record or enqueue it, and respond. Long database workflows, API calls, image processing, email delivery, or report generation belong after acknowledgment.

This reduces retry storms and makes rate limiting easier. It also limits how much computation an unauthenticated internet-facing endpoint can force before authentication and validation are complete.

Validate the payload after authentication

A valid signature proves the payload came through the provider's signing system. It does not guarantee every field is safe for your application. Validate the event type, object identifiers, numeric ranges, URLs, expected schemas, and allowed state transitions before acting.

If your system only needs three event types, reject or ignore the others. If an invoice amount is informational and the authoritative amount lives in your database, do not overwrite the database from a webhook field merely because the event was signed.

Watch for SSRF when events contain URLs

A webhook can become an SSRF path when the receiver fetches a URL supplied in the event. Examples include avatar URLs, attachment URLs, callback destinations, preview links, or an integration configuration that allows a customer to set a webhook target.

Do not blindly fetch arbitrary URLs. Restrict schemes, validate the destination after DNS resolution, block loopback, link-local, private, and metadata-service ranges unless they are explicitly required, and consider an allowlist for provider-controlled attachment hosts. Revalidate redirects because an apparently public URL can redirect to an internal address.

Vulnify's public website scanner includes SSRF-related checks at deeper scan levels, but an external scanner cannot prove that every internal webhook path is safe. Server-side URL validation and egress controls remain application responsibilities.

If your application sends webhooks, protect the customer too

A SaaS platform that sends webhooks should generate strong per-endpoint or per-customer signing secrets, document the signature format, support rotation, and include a timestamp and unique delivery identifier. Do not ask customers to trust IP addresses as the only authentication mechanism unless there is a very specific operational reason.

When customers configure callback URLs, validate them. This is another SSRF boundary because your infrastructure will make outbound requests on the customer's instruction. Resolve hostnames safely, restrict private destinations, protect cloud metadata services, enforce HTTPS where appropriate, and recheck redirects.

Use TLS and network controls as supporting layers

Webhook endpoints should use HTTPS with a valid certificate. IP allowlists can reduce noise when the provider publishes stable ranges, but they are usually a supplementary control, not a replacement for cryptographic event authentication.

Rate limit the endpoint by sensible dimensions. A global rate limit that blocks all webhooks during a legitimate burst can create a retry storm. A limit per source, tenant, or endpoint may be more appropriate depending on the architecture.

Keep error responses and logs boring

A webhook endpoint does not need to reveal stack traces, database errors, expected signing strings, or internal object state. Return simple status codes and log enough structured context to diagnose the event without storing secrets or unnecessary personal data.

Keep a correlation ID, provider delivery ID, event type, verification result, processing state, and timestamps. That is usually more useful than a raw body retained forever.

Webhook review checklist

  • HTTPS is required.
  • The provider signature is verified using the exact documented raw-body format.
  • Comparison uses a constant-time helper where appropriate.
  • Signing secrets are stored and rotated securely.
  • Timestamp freshness is enforced.
  • Unique event or delivery IDs are recorded.
  • Processing is idempotent.
  • Long-running work is queued after fast acknowledgment.
  • Payload fields are schema-validated.
  • Business state is checked against authoritative server-side data.
  • URLs in payloads cannot reach internal or metadata services unexpectedly.
  • Callback URL configuration is protected against SSRF.
  • Logs redact secrets and unnecessary sensitive data.
  • Rate limits and monitoring account for normal provider retries.

Public testing around webhook endpoints

Vulnify can help assess the public surface around a webhook-enabled application, including TLS, headers, exposed paths, redirects, and scanner checks relevant to SSRF and other common web weaknesses. It cannot infer your internal event semantics or prove idempotency from an unauthenticated public response.

That distinction is useful. Public-surface testing can reveal exposed configuration and common vulnerability patterns. Webhook authenticity, replay handling, and business-state validation have to be implemented and tested in the application itself.

The design principle

Treat a webhook as an authenticated message arriving over an untrusted network. Verify who sent it, verify that it is fresh, make duplicate processing harmless, validate what it is asking the application to do, and constrain every network request it can cause.

Most webhook incidents are not caused by exotic cryptography. They come from one missing boundary: trusting a payload before verification, processing the same event twice, or allowing a signed event to drive an unsafe server-side action. A small number of disciplined controls prevents a large class of failures.