The Silent Sabotage of Webhook Signatures

Webhooks are a fundamental communication channel for modern applications, enabling real-time data exchange between services. A common security practice is to sign these payloads, typically using HMAC (Hash-based Message Authentication Code), to ensure data integrity and authenticity. The sender computes a signature based on the raw payload bytes and a shared secret, sending both the payload and the signature to the receiver. The receiver then recomputes the signature using the same raw payload and secret. If the computed signature matches the received signature, the payload is considered valid.

However, a critical vulnerability emerges when intermediaries, such as API gateways, load balancers, or even application middleware, modify the webhook's JSON payload before it reaches the final handler. This modification, often subtle – changing whitespace, reordering keys, or adding/removing fields – invalidates the original signature. The problem is insidious: the object may be semantically equivalent to the sender's original data, but its serialized representation has changed. This means a signature computed on the original, untampered bytes will never match a signature computed on the rewritten bytes, even if the underlying data is logically the same.

Consider a scenario where a service sends a webhook notification about a new user signup. The payload might look like this:

{
  "event": "user.created",
  "data": {
    "id": 123,
    "name": "Alice",
    "email": "alice@example.com"
  },
  "timestamp": 1678886400
}

The sender calculates an HMAC signature over these exact bytes. If an API gateway intercepts this webhook, and its configuration dictates that all JSON keys must be sorted alphabetically for consistency, the payload might be rewritten to:

{
  "data": {
    "email": "alice@example.com",
    "id": 123,
    "name": "Alice"
  },
  "event": "user.created",
  "timestamp": 1678886400
}

Even though the data is identical, the byte sequence has changed due to the key reordering. A verifier on the receiving end, attempting to validate the signature against the *rewritten* payload, will fail because the signature was generated from the *original* payload's byte sequence.

The Root Cause: Serialized Representation vs. Semantic Equivalence

The core issue lies in how webhooks and their signatures are typically handled. Signature verification is usually performed on the raw, incoming request body. If any part of the request processing pipeline, from the edge proxy to the application's own middleware, alters this body before verification, the signature becomes meaningless. This is because the cryptographic hash function used in HMAC is sensitive to every single bit of the input. A single bit flip, or a change in byte order, results in a completely different hash.

Many modern applications use proxies or API gateways for various reasons: SSL termination, rate limiting, request routing, logging, or even transforming payloads to fit downstream services. While these functions are valuable, they must be implemented with an understanding of webhook security. If a proxy rewrites the JSON, it effectively strips the sender's guarantee of authenticity and integrity.

Mitigation Strategies: Preserving the Signed Input

The most robust solution is to ensure that signature verification happens *before* any middleware or proxy attempts to parse, modify, or rewrite the request body. This means the verification logic must operate on the original, unaltered bytes of the webhook payload.

1. Verify Early in the Request Lifecycle

The ideal pattern is to verify the signature immediately upon receiving the request, using the raw request body. If verification passes, only then should the payload be parsed and potentially transformed for internal use. This requires careful ordering of middleware or configuration within API gateways.

A typical verification function in Node.js might look like this:

import crypto from "node:crypto";

export function verify(rawPayload, signatureHeader, secret) {
  const hmac = crypto.createHmac('sha256', secret);
  hmac.update(rawPayload);
  const expectedSignature = hmac.digest('hex');

  // Use a timing-safe comparison to prevent timing attacks
  return crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expectedSignature));
}

This function takes the raw payload string, the signature from the header, and the shared secret. It computes the expected signature and compares it securely against the received one. The crucial point is that `rawPayload` must be the exact, unmodified string received from the network.

2. Proxy Configuration and Webhook Design

If using a proxy or API gateway that modifies payloads, several approaches can be taken:

  • Bypass Transformation for Signed Payloads: Configure the proxy to skip JSON rewriting or modification for requests destined for webhook endpoints that require signature verification. This might involve specific routing rules or header checks.
  • Forward Original Payload: Some advanced proxies can be configured to forward the original, raw request body to the application endpoint in addition to or instead of the transformed body. The application can then use this raw body for verification.
  • Re-signing (Less Ideal): A proxy could theoretically re-sign the payload after modification. However, this requires the proxy to have access to the original secret, which can be a significant security risk. It also adds complexity and potential for error. This is generally not recommended.
  • Sender-Side Robustness: If possible, design webhook payloads to be less sensitive to serialization changes. For example, always include a canonical representation or ensure timestamps are handled consistently. However, this doesn't solve the fundamental signature mismatch problem.

3. Timestamp Verification

Many webhook signature schemes also include a timestamp in the payload or as part of the signed data. This adds another layer of security by preventing replay attacks. The receiver verifies that the timestamp is recent (e.g., within the last 5 minutes). This timestamp is also susceptible to modification by proxies, further underscoring the need to verify the entire signed payload before any alterations.

The Unanswered Question: Who Owns the Security of the Chain?

What remains unclear is how developers should universally address this problem when they don't control the entire network path. If a third-party API provider changes its webhook payload serialization, or if an infrastructure team deploys a new proxy configuration, applications relying on webhook security can be compromised without their knowledge. This highlights a broader challenge in distributed systems security: ensuring end-to-end integrity when components outside direct control can introduce subtle, yet critical, modifications.

Conclusion

Webhook signature verification is a vital security mechanism. When proxies or middleware rewrite JSON payloads, they break this mechanism by altering the data on which the signature was calculated. Developers must prioritize verifying signatures against the raw, unaltered payload before any parsing or transformation occurs. This requires careful middleware ordering, robust proxy configuration, and a deep understanding of the request lifecycle. Failure to do so leaves applications vulnerable to spoofed or tampered webhook data, undermining trust and potentially leading to significant security breaches.