Skip to content
stateless.co · Engineering notes from the request/response layer
statelessThe engineering desk

A publication about the machinery under everyday software: the contracts between services, the queries behind a page, and the failures that only show up in production.

05Security & Identity

HMAC Webhook Signatures: The Four Mistakes That Pass Code Review

Webhooks pass event data between client and server with clear security guarantees.

HMAC Webhook Signatures: The Four Mistakes That Pass Code Review
Photo: Sussex Archaeological Society, Liz Wilson, 2004-06-11 12:13:34 · CC BY-SA 4.0 · Wikimedia Commons
What is in this piece
  1. Byte precision needed at every step
  2. The time-bound signature window
  3. The replay defense
  4. The four twists
  5. Trust the endpoint, not the response

Byte precision needed at every step

The first job of the webhook verifier is to pick the same input on which the sender’s signature is computed. HMAC signature takes a separating step:

  1. The webhook provider calculates the HMAC over the binary representation of the payload and the binary representation of the HMAC key, then Base64-encodes the result. The exact bytes are crucial: deserialisation alters the bytes, so the receiver must verify the signature before parsing.

For example, Adyen says to compute the HMAC over the "binary representation of the payload" and their instructions use JSON display-specific terms like "convert into a string acceptable by JSON":

Enjoy precise signature verification on all supported platforms: compute the HMAC over the binary representation of the payload along with the HMAC key, then Base64-encode the result and compare it to the value of the signature header.

  1. Adyen specifies the webhook data must be left "as it is":

You receive the exact string that was used in the HMAC calculation, so that you can validate the integrity and source of the message. To validate the integrity of the message, you need to recreate the HMAC signature from the original message body string and compare the result with the MD5 field from the notification.

Adyen specifically warns:

Make sure to not remove the dots (.) or line breaks in the message body.

  1. Most webhook providers share similar guidance. Webhook Relay advises:

Consider the raw request body for HMAC-SHA256(secret, rawBody) but do not decode it.

Paddle says to pass the "raw request body" and doesn’t support JSON.parse first:

To prevent issues with different parsers, {Hookdeck} assumes the raw request-body string comes in the request (as a U8 array).

The time-bound signature window

Webhook signatures must survive replay. That needs a timestamp in the body. For example, the NovaVMS cryptographic operation uses a timestamp to tie the request to a timeframe:

A strong point is that the secret can be rotated and used as a timeslice key.

Hooklistener advises:

timestamp must be part of what’s signed, otherwise if an attacker knows how the signature is crafted, they can change what’s being signed, making the webhook no longer secure

The replay defense

Many providers reject some Hmac webhook signatures. For example, Hooklistener describes rejecting ones too old:

Note – reject stale 'ts' in the timestamp – clock skew or time to deliver has repercussions here – ALWAYS check the received timestamp. Hookdeck reads a standard Unix timestamp from the 'ts' header, adjusts for clock skew and checks against a present (now - max_window) range, and saves a portion to be hashed for the signature.

Paddle calls out a specific time window:

Note: Webhook request bodies are in raw utf-8 form and a Unix timestamp <ts> is attached in seconds to the secret key for cryptographic signatures.

NovaVMS specifies that after an algorithm change, signatures will be validated with both current and previous secrets during rotation:

It’s really nice to to be able to rotate e.g. we are planning to run a migration from the current Adyen webhook signing to a stronger webhook signing in future. It should be backward compatible where we can support the current and previous signatures on receipt, to prevent interruption of processing.

Hooklistener expands on accepting multiple:

As a final point, for all these cases, activated keys are always advanceable. Do perform some sort of secret rotation, that's ok. Even if it does kick out any existing keys – that's handled here because we never accept a key field for ourselves, we always search our store.

The four twists

These four details can create a webhook verification that meets the narrow requirements with no loopholes:

1. 2.

  1. Defining the predefined replay window and the time-based window out not found

4.

Trust the endpoint, not the response

Verification never trusts request properties that are not signed over explicitly. Always use exactly the endpoint documentation, never the delivered byte string.

If any unspecified field of any vendor-supplied signature specifies an algorithm, it requires further careful scrutiny. Whether the vendor applies best practices crypto with regard to theirs doesn't imply so.

Adyen warns

Avoid people calling with algorithm=MD5 for example.

Hookdeck prescribes, verbatim:

With regards to "accepting the algorithm" I'd propose to not–ever do something that isn't guaranteed. specific algorithm acceptance guidance...