HMAC-Signed Webhooks: The Security You Should Never Hand-Roll

An unsigned webhook endpoint is a door anyone can knock on pretending to be your platform. The correct defence — HMAC-SHA256 over the exact raw body with a shared secret, verified in constant time — is easy to get subtly wrong. Why you shouldn’t build it yourself, and how VBWD signs every delivery (X-VBWD-Signature and friends) so verification is a few honest lines on your side.

An unsigned webhook endpoint is a door anyone can knock on pretending to be your platform.

Here is an uncomfortable fact about webhooks: the moment your platform starts sending them, you have also started accepting HTTP requests from the internet that carry instructions your systems will act on. A webhook that says payment.captured can trigger a fulfilment. One that says subscription.cancelled can revoke access. If a receiver believes any POST that shows up at its endpoint, then anyone who learns the URL — and URLs leak, into logs, proxies, browser history, screenshots in support tickets — can forge those events at will.

The forgery problem, stated plainly

A webhook endpoint is, by design, a publicly reachable door. You cannot hide it behind your auth wall, because the whole point is that an external service calls it unprompted. So the question is not “how do I keep strangers out?” but “how does my receiver know a given request genuinely came from the platform I trust, and hasn’t been tampered with on the way?”

The naive answers all fail. A shared secret in the URL query string ends up in access logs. Checking the source IP breaks the day your provider changes hosting and is spoofable in plenty of network setups. Requiring HTTPS protects the data in transit but proves nothing about who sent it. None of these give you what you actually need: cryptographic proof of origin and integrity, computed over the exact bytes you received.

Why HMAC-SHA256 over the raw body is the answer

The established, boring, correct solution is a message authentication code. The sender and receiver share a secret. For every delivery, the sender computes HMAC-SHA256 over the raw request body using that secret, and puts the result in a header. The receiver recomputes the same HMAC over the bytes it received and compares. If the two match, two things are simultaneously true: the request came from someone holding the secret, and not a single byte of the payload was altered in flight. Change one character of the JSON and the signature no longer matches.

Three details separate a real implementation from a broken one, and every one of them is easy to get subtly wrong:

  • Sign the raw body, not a parsed-and-re-serialised copy. The instant you JSON.parse and re-stringify, key order and whitespace can shift, and your recomputed signature drifts from the sender’s. Verify against the exact bytes off the wire, before any framework touches them.
  • Compare in constant time. A naive == on the signature strings returns as soon as it hits a mismatching character, and that timing difference is measurable. An attacker can exploit it to recover a valid signature byte by byte. Use a constant-time comparison — hmac.compare_digest in Python, crypto.timingSafeEqual in Node.
  • Include a timestamp and reject stale deliveries. Signing the body proves integrity but not freshness; without a timestamp check, a captured-and-replayed request is still perfectly valid. A timestamp header, also covered or checked alongside the signature, lets you refuse anything older than a small window.

None of this is exotic. It is a decade of settled practice. And that is exactly why hand-rolling it is a poor use of your engineering time: the algorithm is standard, the failure modes are well known, and there is no competitive advantage in reimplementing it — only the risk of shipping one of the three mistakes above and not finding out until an incident.

Verify it, don’t build it

This is where a platform earns its keep. In VBWD, every outbound webhook is HMAC-SHA256 signed over the exact JSON body with a per-subscription secret, and the delivery carries the headers you need to verify it: X-VBWD-Signature, X-VBWD-Event, X-VBWD-Delivery-Id and X-VBWD-Timestamp. Your side of the contract is one small, boring function:

import hmac, hashlib

def verify(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        raw_body,               # the exact bytes received
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature)  # constant-time

That is the whole receiver-side security model, and it is the same regardless of which event fired. Because the signing lives in the platform’s agnostic outbound relay rather than in any one feature, a subscription.activated event and a plugin’s invoice.paid event are signed identically — you write the verify once and trust everything.

An honest caveat: signature verification proves a request is authentic and untampered. It is not the entire job. You still want idempotent handlers (the same delivery can legitimately arrive twice on a retry), a timestamp window to close replay, and secrets kept out of source control. HMAC is the foundation, not the finished house — but it is the part you should never reinvent, and with VBWD you don’t. Because VBWD is self-hosted and source-available under BSL 1.1 (free for commercial use while annual VBWD-attributable sales stay under the value of 6.7 BTC per year), the secrets and the signing code both live inside your own perimeter, where you can read them.

If you want the mechanics of how the events get published and delivered in the first place, the deep-dive is here: Webhooks in VBWD, Part 1: the event-driven engine. And the broader feature set sits on the features page.

If this maps to a conversation happening inside your own organisation, the useful next step is specific rather than generic: see your own workload on modern, self-hosted infrastructure. Request an enterprise installation and bring the numbers you’re trying to improve.

VBWD is source-available — get the SDK on GitHub.

Happy
Happy
0%
Sad
Sad
0%
Excited
Excited
0%
Angry
Angry
0%
Surprise
Surprise
0%
Sleepy
Sleepy
0%

Event-Driven Architecture, Explained for Founders

Your Zapier and Make Automations Are Only as Good as Your Platform’s Events

Leave a Reply

Your email address will not be published. Required fields are marked *

four × two =