Est.
FeaturesLong read

Webhook Signature Verification: What HMAC Alone Misses

HMAC signatures alone cannot prevent replay attacks, timing leaks, or server-side request forgery.

· 9 min read
Cover illustration for “Webhook Signature Verification: What HMAC Alone Misses”
Features · November 2, 2026 · 9 min read · 1,969 words

Webhook signature verification is a necessary first layer. HMAC can authenticate signed bytes, but it cannot establish freshness, prevent duplicate processing, make an unsafe comparison safe, or protect a sender from hostile delivery URLs. Closing those gaps takes separate controls.

What HMAC signature verification actually proves — and what it does not

HMAC-SHA256 combines the raw request body with a shared secret to produce a signature. In well-designed schemes, the signed content also includes a timestamp. The receiver computes the same value and compares it with the supplied signature. A match authenticates the signed bytes, assuming the secret remains confidential. It does not prove that confidentiality by itself.

That boundary is narrow, and deliberately so. HMAC does not establish whether this is the first delivery, whether the event has already been processed, or whether the comparison leaks information. Nor does it make a framework preserve the original bytes. A captured signed request can pass again because repetition changes neither its content nor its signature.

Four distinct gaps that a passing HMAC check leaves open

Gap 1 is replay of a valid request. Without a timestamp window check, an attacker can resend a captured event hours, days, or weeks later.

Gap 2 is the timing side-channel in comparison. Ordinary string comparisons, such as JavaScript’s === or Python’s ==, can stop at the first mismatch. Differences in response time can reveal information about the expected signature, allowing an attacker to work toward a match byte by byte.

Gap 3 is a mismatch between raw and parsed bodies. Providers sign exact bytes, but parsing JSON and serializing it again can change whitespace or key order. Verification then fails, and disabling the troublesome check turns a formatting problem into an authentication bypass.

Gap 4 moves to the producer’s side. A platform that accepts tenant-supplied delivery URLs can be induced to send requests to internal services, including the AWS metadata endpoint at 169.254.169.254. This is server-side request forgery, or SSRF, and signing the outbound payload does nothing to make its destination safe.

What happens when these gaps are left open in production

CVE-2025-11271 affected Easy Digital Downloads and received a CVSS score of 5.3. PayPal IPN verification was unconditionally skipped when the POST body contained verification_override=1, allowing unauthenticated actors to submit forged notifications treated as verified. To close that bypass, verification must stay on regardless of what the request contains.

CVE-2026-41432 affected new-api and received a CVSS score of 7.1. An empty or unset webhook secret caused HMAC-SHA256 to use an empty key, making signatures publicly computable. Attackers could forge checkout.session.completed events to add as much quota as they wanted without paying. The corrective requirement is straightforward but essential. An empty secret must not become an accepted signing key.

CVE-2026-32096 affected Plunk, received a CVSS score of 9.3, and was disclosed in March 2026. An unauthenticated attacker could force server-side requests to internal networks, including the AWS metadata endpoint, and obtain IAM credentials in plaintext. That failure calls for controls on where outbound requests can go, which stronger signatures cannot provide. HashiCorp’s HCSEC-2026-09 separately disclosed exposure of HCP Terraform and Terraform Enterprise GitHub webhook secrets, showing how credential leakage undermines otherwise valid authentication.

These incidents document distinct failures among the gaps above. Subscription activation, payment confirmation, CI/CD triggers, and user provisioning become targets when verification is missing or can be bypassed. The underlying trust boundary is easy to miss. HTTP has no mechanism that forces a receiver to check a signature header. The provider signs, but the receiver decides whether to verify.

Closing Gap 2 first: constant-time comparison as a non-negotiable baseline

The first corrective step is the smallest one. Constant-time digest comparison avoids stopping at the first mismatch, which can reveal matching signature bytes. In Node.js, the wrong pattern is an ordinary === comparison between signature strings. The replacement is crypto.timingSafeEqual() on fixed-length digest buffers, after checking their lengths. That length check is necessary because the function throws when the buffers differ in size.

Python’s documentation recommends hmac.compare_digest() for digest comparison, replacing ==. This is a baseline for cryptographic housekeeping. It addresses the comparison itself, rather than promising that every part of the handler has identical timing. The receiver still needs to reject malformed signature input and compute the expected digest over the correct content.

That content requirement also closes Gap 3. Verification must use the raw body and signature headers before parsing JSON, with the signing key selected from the active keys. The receiver must also check the timestamp and key validity before accepting the request. Parsing before authentication makes the application check its own reconstruction of the message the provider signed.

Closing Gap 1: timestamp windows and idempotency keys as a two-part replay defense

A signed timestamp gives the receiver a freshness test. The receiver rejects an event when its timestamp falls outside the tolerance window around the current time. Five minutes is a common convention. However, a request replayed inside the window still has a valid signature and an acceptable timestamp.

An idempotency store covers that remaining opening. The receiver records processed delivery IDs in Redis or Postgres and checks incoming IDs before processing. If an ID is already in the store, the delivery is a duplicate even if its timestamp is still fresh. Some providers send a unique nonce with each delivery, which the receiver can also track to spot duplicates. The store is what gives the receiver memory.

Legitimate retries make the two controls interdependent. A provider retry arriving after a tight window can be rejected despite being genuine, so a timestamp-only implementation may discard work. Idempotency is the durable defense, while the timestamp is the coarse outer filter. A duplicate store does not make an expired request pass, so the tolerance still needs to accommodate legitimate retries.

Standard Webhooks signs id.timestamp.body, binding the delivery ID to the timestamp and payload. An attacker therefore cannot swap the ID without invalidating the signature.

Closing Gap 4: SSRF defenses for platforms that emit webhooks

The previous controls protect receivers from incoming requests. Platforms that let tenants register outbound webhook URLs face a different problem, because those URLs tell the platform where to connect. An internal destination can turn a routine delivery into a request for credentials. A signed envelope can still have a dangerous address.

The producer-side defense begins with DNS pinning. Resolve the hostname at registration, verify that every resolved IP is public, and use the validated IP for the connection. The delivery call must not independently resolve the hostname again. Otherwise, the request can reach a destination other than the one that was checked.

MLflow’s _validate_webhook_url illustrates that check-to-connect gap. It resolved the hostname and checked for public addresses, but discarded the resolved IP before the HTTP call resolved it again. DNS rebinding exploited that separation between validation and use. The additional controls are to block RFC-1918 and link-local ranges at the egress proxy, allow only HTTPS, and consider known-good CIDR allowlists for high-trust integrations.

Secret lifecycle: rotation, zero-downtime overlap, and the empty-secret failure mode

Checks on each request need a signing key that works and is kept secret. A leaked key permits forgery, while an empty key makes the expected signature publicly computable. A green verification result is useless when anyone can produce it.

Rotation should be an automated infrastructure task. API-driven rotation allows CI pipelines to run it on a schedule. During a planned transition, Standard Webhooks supports signing with both the current and old keys and placing the signatures, space-delimited, in the webhook-signature header. The receiver tries the supported signatures until one matches, allowing migration without delivery downtime.

That overlap needs a defined end. A key ID, or kid, lets receivers select from a small set of current and recently rotated keys. Unknown or expired key IDs should be rejected. Short-lived per-delivery or per-batch tokens derived from a rotating root secret offer a stricter form of limited credential lifetime.

Logging discipline belongs in this lifecycle as well. The GitHub incident from September 11 to December 10, 2025 exposed signing secrets through response headers logged by receiving systems, enabling forgery. The operational rule is to never log signature headers or raw webhook secrets. Rotation cannot undo disclosure while a compromised key remains accepted.

HMAC versus asymmetric signing: when the shared-secret model is the wrong tool

HMAC suits most webhook integrations because it is fast, well understood, and compatible with shared-secret provisioning. Anyone able to verify a stored payload also holds a secret capable of signing it. The receiver therefore cannot prove to an auditor that the sender, rather than the receiver, produced those bytes.

Rotation creates a related archival problem for HMAC. Old payloads become uncheckable unless the retired secrets remain available, which works against retiring credentials. Ed25519 signing separates those roles by giving the receiver only a public key. Archived messages remain verifiable while the old public key is published, without giving verifiers the ability to forge new signatures.

Standard Webhooks defines Ed25519 as v1a and HMAC-SHA256 as v1, and prefers asymmetric signatures. Browser verification and third-party redistribution are examples where verifiers should not hold signing secrets. Neither asymmetric signatures nor HMAC replaces replay controls or safe delivery destinations, and mTLS is not a substitute framing for this choice.

Post-quantum planning is a separate concern for the longer term. HMAC-SHA256 is not vulnerable to Shor’s algorithm, while Ed25519 is not quantum-resistant. Teams planning cryptographic changes should use an alg or version header prefix to identify the algorithm. That lets verification change over time without forcing teams to move HMAC receivers to a new algorithm in a rush.

GDPR exposure hidden inside webhook payloads and delivery logs

Authentication does not establish a legal basis for processing. Even when a webhook is signed and handled correctly, forwarding, logging, or keeping it can still expose personal data. Security checks cannot authorize recipients who should never have received the payload.

Delivery logs should be limited to metadata. Suitable fields include event IDs, subscription IDs, delivery outcomes, and retry counts. Boundary logs need a defined short retention window, with an explicit legal basis for anything longer. PII-redacted summaries can support longer-term trend analysis where that basis exists.

Processing and storing payloads on EU infrastructure reduces cross-border transfer exposure and simplifies sub-processor disclosure requirements. It does not replace control over the fields entering an application. Schema and event-type allowlists provide that boundary, rejecting malformed payloads and preventing unexpected PII fields from entering the pipeline when a provider expands an existing event.

The complete layered defense stack and where most implementations stop short

The complete defense has six controls with separate jobs. First comes constant-time HMAC verification over the raw body, followed by a signed timestamp and tolerance window. Third is the idempotency store, which detects duplicates within and across that window. Many implementations stop at the first or second layer, sometimes retaining an ordinary string comparison or omitting durable duplicate tracking.

Fourth is SSRF-aware egress with IP pinning at connection time for platforms that send webhooks. Fifth is a schema and event-type allowlist. Sixth is per-endpoint secret rotation with an overlap window and key IDs, limiting the reach of a compromised credential without interrupting a planned transition.

Processing behavior also belongs in the operational design. Acknowledging quickly with HTTP 200 or 202 and pushing work to a queue reduces exposure to slow-handler denial of service. It also keeps work that takes a long time from causing timeouts that vendors treat as failed deliveries. Authentication is not an excuse to keep the sender waiting at reception.

Standard Webhooks provides an external reference for signed identifiers, timestamps, algorithm versions, and rotation overlap. A platform layer can absorb the idempotency store, SSRF-safe egress, and secret rotation, leaving application code responsible for verification, timestamp checks, and payload allowlists. That division makes complete protection practical without mistaking a passing HMAC check for a finished security design.

More in Features