Keine Werbung mögen? Gehen Werbefrei Heute

HMAC How Webhooks Know You’re Not Lying

Veröffentlicht am

Jedes Mal, wenn GitHub, Stripe oder Shopify ein Webhook an Ihren Server senden, signieren sie den Payload mit HMAC. Hier ist genau, wie das funktioniert – und wie man es in eigener Code-Logik überprüft.

HMAC: How Webhooks Know You're Not Lying 1
ANZEIGE Entfernen?

You receive a POST request claiming to be from Stripe. The payload says a payment just succeeded. You process the order, ship the goods — and three days later you discover the request was faked. Ouch.

This is why every serious webhook provider signs their payloads using HMAC. If you understand HMAC, you understand why GitHub, Stripe, Shopify, Twilio, and practically every modern API uses it — and you’ll know exactly how to verify those signatures in your own server code.

What HMAC actually is

HMAC stands for Hash-based Message Authentication Code. It answers one question: “Did the person who sent me this message know the shared secret?”

It works by running a cryptographic hash function — usually SHA-256 — over the combination of a secret key and the message body. The output is a fixed-length string that:

  • Changes completely if even one byte of the message changes
  • Cannot be produced without knowing the secret key
  • Cannot be reversed to reveal the key or the original message

The formula is compact: HMAC(key, message) = H((key ⊕ opad) || H((key ⊕ ipad) || message)). You don’t need to memorise the internals — every language ships a standard library implementation — but it helps to know the intent: the key is mixed into the hash twice, in different ways, to prevent a class of attacks called length-extension attacks.

How a webhook provider uses it

When you register a webhook endpoint, the provider gives you a signing secret — a random string only you and they know. When an event fires:

  1. The provider serialises the event payload to JSON (or a canonical string).
  2. It computes HMAC-SHA256(secret, payload).
  3. It sends the request with the signature in a header — X-Hub-Signature-256 for GitHub, Stripe-Signature for Stripe, and so on.

On your end, you do the same computation over the raw request body and compare. If they match, the payload is authentic. If they don’t, discard it.

Verifying in code

Here’s what verification looks like across the most common languages. The pattern is identical in every one: compute, compare with a constant-time function.

Node.js

const crypto = require('crypto');

function verifyWebhook(rawBody, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)          // rawBody must be a Buffer or string — NOT parsed JSON
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

Python

import hmac
import hashlib

def verify_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        raw_body,
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(expected, signature)

PHP

function verifyWebhook(string $rawBody, string $signature, string $secret): bool {
    $expected = hash_hmac('sha256', $rawBody, $secret);
    return hash_equals($expected, $signature);
}

The critical detail: use the raw body

The most common implementation mistake is passing a parsed and re-serialised payload to the HMAC function instead of the original raw bytes. Key ordering, whitespace, and Unicode escaping all affect the hash. The provider signed the exact bytes it sent over the wire — you must hash exactly those same bytes.

In Express (Node.js), that means configuring the body parser to preserve the raw buffer:

app.use('/webhooks', express.raw({ type: 'application/json' }));

In Django, use request.body rather than request.data. In Flask, use request.get_data().

Why not just a plain hash?

A plain SHA-256 of the payload proves nothing — anyone can compute SHA256(payload) without a secret. HMAC’s secret key is what makes it an Authentifizierung code, not just a checksum. It answers “who sent this” rather than just “was this corrupted in transit”.

Why not asymmetric signatures (like RSA)?

RSA and ECDSA let the receiver verify a signature without knowing the private key — that’s valuable for public broadcast (like code-signing). For webhooks, there are only two parties who ever need to verify the signature: you and the provider. A shared secret is simpler, faster, and equally secure in that model. Some providers (Svix, Clerk) do offer asymmetric webhook signing for cases where you can’t safely store a secret server-side.

Replay attacks — and how to stop them

A valid HMAC signature proves authenticity but not freshness. An attacker who captures a legitimate signed request can replay it later. Stripe counters this by including a timestamp in the Stripe-Signature header and hashing the timestamp alongside the payload body. On your side, you reject any request where the timestamp is more than five minutes old.

If you’re building your own webhook system, do the same: include a monotonically increasing nonce or a Unix timestamp in the signed message, and reject stale requests server-side.

Timing-safe comparison is not optional

Never compare HMAC signatures with a plain equality check (===, ==). Short-circuit string comparison leaks information about how many leading bytes match — an attacker making thousands of requests can reconstruct the expected signature one byte at a time. Always use a constant-time comparison:

  • Node.js: crypto.timingSafeEqual()
  • Python: hmac.compare_digest()
  • PHP: hash_equals()
  • Go: hmac.Equal()
  • Ruby: ActiveSupport::SecurityUtils.secure_compare()

Putting it together: a production-ready webhook handler

Here’s a complete example using GitHub’s X-Hub-Signature-256 header in Node.js:

const express = require('express');
const crypto = require('crypto');

const app = express();
const WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET;

// Keep the body as raw bytes — critical!
app.use('/github/webhook', express.raw({ type: 'application/json' }));

app.post('/github/webhook', (req, res) => {
  const sigHeader = req.headers['x-hub-signature-256'];
  if (!sigHeader) return res.status(401).send('Missing signature');

  const sig = sigHeader.replace('sha256=', '');
  const expected = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(req.body)
    .digest('hex');

  if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(req.body);
  // Safe to process the event now
  console.log('Event type:', req.headers['x-github-event']);

  res.sendStatus(200);
});

app.listen(3000);

Quick reference: who uses what

ProviderKopfbereichAlgorithmusTimestamp in signature?
GitHubX-Hub-Signature-256HMAC-SHA256NEIN
StripeStripe-SignatureHMAC-SHA256Ja
ShopifyX-Shopify-Hmac-Sha256HMAC-SHA256NEIN
TwilioX-Twilio-SignatureHMAC-SHA1NEIN
LockerX-Slack-SignatureHMAC-SHA256Ja
PaddlePaddle-SignatureHMAC-SHA256Ja
Möchten Sie werbefrei genießen? Werde noch heute werbefrei

Erweiterungen installieren

IO-Tools zu Ihrem Lieblingsbrowser hinzufügen für sofortigen Zugriff und schnellere Suche

Zu Chrome-Erweiterung Zu Kantenerweiterung Zu Firefox-Erweiterung Zu Opera-Erweiterung

Die Anzeigetafel ist eingetroffen!

Anzeigetafel ist eine unterhaltsame Möglichkeit, Ihre Spiele zu verfolgen. Alle Daten werden in Ihrem Browser gespeichert. Weitere Funktionen folgen in Kürze!

ANZEIGE Entfernen?
ANZEIGE Entfernen?
ANZEIGE Entfernen?

Nachrichtenecke mit technischen Highlights

Beteiligen Sie sich

Helfen Sie uns, weiterhin wertvolle kostenlose Tools bereitzustellen

Kauf mir einen Kaffee
ANZEIGE Entfernen?