Webhook-Sicherheit: HMAC-Signaturen, Idempotenz und Wiedergabeschutz
Drei Codemuster stoppen gefälschte Payloads, doppelte Zustellungen und wiederholte Webhook-Anfragen. Node.js-Beispiele mit HMAC-SHA256 und Zeitstempelprüfungen.
Ihr Webhook-Handler akzeptiert eine POST-Anfrage, analysiert den JSON und führt die Geschäftslogik aus. Das funktioniert, bis jemand sendet eine gefälschte Nutzlast an Ihren Endpunkt. Oder bis der Anbieter eine Zustellung erneut versucht und Ihr Zusteller eine Gebühr berechnet Kunde zweimal. Oder bis ein Angreifer eine legitime Anfrage aufzeichnet und sie sechs Stunden später erneut abspielt.
Drei Schutzmaßnahmen beheben dieses Problem: HMAC-Signaturüberprüfung, Idempotenzschlüssel und zeitstempelbasierte Wiedergabeablehnung. In diesem Tutorial wird jedes einzelne davon mit funktionierendem Node.js-Code behandelt.
So funktioniert die Webhook-Signierung
Jeder große Webhook-Anbieter (Stripe, GitHub, Shopify, Twilio) signiert ausgehende Payloads mit HMAC-SHA256. Die Der Anbieter kombiniert den rohen Anforderungstext mit einem gemeinsamen Geheimnis, berechnet einen Hash und sendet diesen Hash in einem Header. Ihr Server berechnet den Hash mit demselben Geheimnis neu und vergleicht ihn. Wenn die Hashes übereinstimmen, ist die Nutzlast authentisch.
Die Signaturformel sieht so aus:
HMAC-SHA256(secret, timestamp + "." + raw_body) = signature
Stripe sendet die Signatur ein Stripe-Signature. GitHub sendet es ein X-Hub-Signature-256.
Der Algorithmus ist derselbe; Lediglich der Name und das Format des Headers unterscheiden sich.
Überprüfen Sie eine Stripe-Webhook-Signatur in Node.js
Streifen Stripe-Signature Header enthält einen Zeitstempel (t=) und eine oder mehrere versionierte
Unterschriften (v1=). So überprüfen Sie es ohne das Stripe SDK:
const crypto = require('crypto');
function verifyStripeSignature(payload, header, secret) {
const parts = header.split(',');
const timestamp = parts.find(p => p.startsWith('t=')).slice(2);
const signature = parts.find(p => p.startsWith('v1=')).slice(3);
const signedPayload = timestamp + '.' + payload;
const expected = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
if (expected !== signature) {
throw new Error('Invalid signature');
}
// Replay protection: reject if older than 5 minutes
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
if (age > 300) {
throw new Error('Timestamp too old');
}
return JSON.parse(payload);
}
Wichtige Details: Stripe verkettet den Zeitstempel und den Rohtext mit einem Punkttrennzeichen. Der v1
Die Signatur verwendet HMAC-SHA256. Die Zeitstempelprüfung am Ende bietet einen Wiederholungsschutz (siehe unten).
Überprüfen Sie eine GitHub-Webhook-Signatur in Node.js
Das Format von GitHub ist einfacher. Der X-Hub-Signature-256 Header enthält sha256= gefolgt
durch den Hex-HMAC des Rohkörpers:
const crypto = require('crypto');
function verifyGitHubSignature(payload, signatureHeader, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
const trusted = Buffer.from(expected, 'utf8');
const untrusted = Buffer.from(signatureHeader, 'utf8');
if (trusted.length !== untrusted.length) {
throw new Error('Invalid signature');
}
if (!crypto.timingSafeEqual(trusted, untrusted)) {
throw new Error('Invalid signature');
}
return JSON.parse(payload);
}
Beachten crypto.timingSafeEqual. Ein Standard === Der Vergleich lässt Timing-Informationen durchsickern.
Ein Angreifer kann Antwortzeiten messen, um Byte für Byte die richtige Signatur zu erraten. Zeitkonstanter Vergleich
eliminiert diesen Vektor.
Berechnen Sie HMAC-Signaturen mit der Hash-API von botoi
Wenn Sie HMAC-SHA256-Signaturen aus einem Skript, einer CI-Pipeline oder einer serverlosen Funktion berechnen oder überprüfen müssen
ohne zu importieren crypto, verwenden Sie die /v1/hash/hmac Endpunkt:
curl -X POST https://api.botoi.com/v1/hash/hmac \\
-H "Content-Type: application/json" \\
-d '{
"text": "1712345678.{\\"event\\":\\"payment_intent.succeeded\\",\\"amount\\":4999}",
"key": "whsec_your_stripe_secret",
"algorithm": "sha256"
}'
Antwort:
{
"success": true,
"data": {
"hmac": "a3f2b8c9d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0",
"algorithm": "sha256"
}
}
Dies gibt den hexadezimal codierten HMAC zurück. Vergleichen Sie ihn mit dem Signaturheader des Webhook-Anbieters.
Der Endpunkt unterstützt beides sha256 Und sha512 Algorithmen.
Erstellen Sie eine Express-Middleware für die HMAC-Verifizierung
Binden Sie die Signaturprüfung in wiederverwendbare Middleware ein, damit jede Webhook-Route geschützt wird, ohne dass Code dupliziert werden muss:
const crypto = require('crypto');
function webhookAuth(secret) {
return (req, res, next) => {
const signature = req.headers['x-webhook-signature'];
const timestamp = req.headers['x-webhook-timestamp'];
if (!signature || !timestamp) {
return res.status(401).json({ error: 'Missing signature headers' });
}
// Step 1: Replay protection
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
if (age > 300) {
return res.status(401).json({ error: 'Request too old' });
}
// Step 2: HMAC verification
const payload = JSON.stringify(req.body);
const expected = crypto
.createHmac('sha256', secret)
.update(timestamp + '.' + payload)
.digest('hex');
const trusted = Buffer.from(expected, 'utf8');
const untrusted = Buffer.from(signature, 'utf8');
if (trusted.length !== untrusted.length ||
!crypto.timingSafeEqual(trusted, untrusted)) {
return res.status(401).json({ error: 'Invalid signature' });
}
next();
};
}
// Mount on your webhook route
app.post('/webhooks/stripe', webhookAuth(process.env.STRIPE_WEBHOOK_SECRET), handler);
Diese Middleware lehnt Anfragen mit fehlenden Headern, veralteten Zeitstempeln oder ungültigen Signaturen vor Ihrer Anfrage ab Handler läuft. Das 300-Sekunden-Fenster entspricht der Standardtoleranz von Stripe.
Idempotenz: Doppellieferungen überspringen
Webhook-Anbieter wiederholen fehlgeschlagene Zustellungen. Wenn Ihr Server nach der Verarbeitung des Ereignisses, aber vorher, eine 500 zurückgegeben hat Wenn der Anbieter die 200-Antwort sendet, sendet er dasselbe Ereignis erneut. Ohne Idempotenz führt Ihr Handler die aus Nebenwirkungen doppelt: doppelte Gebühren, doppelte E-Mails, wiederholte Bestandsaktualisierungen.
Die Lösung: Speichern Sie jede verarbeitete Ereignis-ID und überspringen Sie Ereignisse, die Sie zuvor gesehen haben.
In-Memory-Idempotenz (Entwicklung)
const processedEvents = new Map(); // Use Redis in production
async function handleWebhook(req, res) {
const eventId = req.body.id;
// Check if already processed
if (processedEvents.has(eventId)) {
return res.status(200).json({ status: 'duplicate', eventId });
}
// Mark as processing before side effects
processedEvents.set(eventId, Date.now());
try {
await processEvent(req.body);
return res.status(200).json({ status: 'processed', eventId });
} catch (err) {
// Remove on failure so retries work
processedEvents.delete(eventId);
return res.status(500).json({ error: 'Processing failed' });
}
}
Der Map arbeitet für die lokale Entwicklung. Verwenden Sie in der Produktion einen gemeinsamen Speicher für alle Serverinstanzen
Sehen Sie den gleichen Satz verarbeiteter Ereignisse.
Redis-gestützte Idempotenz (Produktion)
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
async function handleWebhook(req, res) {
const eventId = req.body.id;
const lockKey = \`webhook:processed:\${eventId}\
SET NX ist atomar: Nur ein Prozess gewinnt die Sperre. Die 72-Stunden-TTL (259200 Sekunden)
Bereinigt automatisch alte Einträge. Wenn die Verarbeitung fehlschlägt, löscht der Handler den Schlüssel, sodass der nächste Wiederholungsversuch erfolgreich sein kann.
Wiedergabeschutz: Veraltete Anfragen ablehnen
Die HMAC-Verifizierung bestätigt, dass die Nutzlast vom Anbieter stammt. Aber eine gültige signierte Anfrage, die von a erfasst wurde Netzwerkangreifer bleibt für immer gültig, es sei denn, Sie fügen eine Zeitprüfung hinzu. Der Wiedergabeschutz lehnt Payloads ab mit Zeitstempeln, die älter als ein Schwellenwert sind.
function rejectStaleRequests(req, res, next) {
const timestamp = parseInt(req.headers['x-webhook-timestamp'], 10);
if (isNaN(timestamp)) {
return res.status(401).json({ error: 'Missing timestamp' });
}
const now = Math.floor(Date.now() / 1000);
const age = now - timestamp;
// Reject anything older than 5 minutes or from the future
if (age > 300 || age < -30) {
return res.status(401).json({ error: 'Timestamp out of range' });
}
next();
}
Die 30-Sekunden-Zukunftstoleranz bewältigt geringfügige Taktabweichungen zwischen Ihrem Server und dem Anbieter. Alles Älter als 5 Minuten oder mehr als 30 Sekunden in der Zukunft werden abgelehnt.
Kombinieren Sie alle drei in einer Middleware
Hier ist die komplette Middleware, die alle drei Prüfungen nacheinander ausführt: Zeitstempel, HMAC, dann Idempotenz.
const crypto = require('crypto');
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
function secureWebhook(secret) {
return async (req, res, next) => {
const signature = req.headers['x-webhook-signature'];
const timestamp = req.headers['x-webhook-timestamp'];
const eventId = req.body?.id;
// 1. Reject missing headers
if (!signature || !timestamp || !eventId) {
return res.status(401).json({ error: 'Missing required headers or event ID' });
}
// 2. Replay protection: reject stale requests
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
if (age > 300 || age < -30) {
return res.status(401).json({ error: 'Timestamp out of range' });
}
// 3. HMAC verification
const payload = JSON.stringify(req.body);
const expected = crypto
.createHmac('sha256', secret)
.update(timestamp + '.' + payload)
.digest('hex');
const trusted = Buffer.from(expected, 'utf8');
const untrusted = Buffer.from(signature, 'utf8');
if (trusted.length !== untrusted.length ||
!crypto.timingSafeEqual(trusted, untrusted)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// 4. Idempotency: skip duplicates
const lockKey = \`webhook:processed:\${eventId}\
Die Reihenfolge ist wichtig. Der Zeitstempel ist die kostengünstigste Prüfung (keine E/A) und wird daher zuerst ausgeführt. HMAC-Verifizierung ist CPU-gebunden, aber trotzdem schnell. Idempotency erfordert einen Redis-Aufruf und wird daher zuletzt ausgeführt. Diese Sequenz lehnt ab die meisten Anfragen zu den niedrigsten Kosten.
Testen Sie Ihren Webhook-Handler mit dem Webhook-Posteingang von botoi
Verwenden Sie Schaltflächen /v1/webhook/inbox Um eine Einweg-URL zu erstellen, berechnen Sie eine HMAC-Signatur mit
/v1/hash/hmac, und senden Sie eine signierte Testnutzlast. Keine Tunnel, kein lokaler Server.
# 1. Create a webhook inbox
INBOX=\$(curl -s -X POST https://api.botoi.com/v1/webhook/inbox/create)
INBOX_ID=\$(echo \$INBOX | jq -r '.data.inbox_id')
INBOX_URL=\$(echo \$INBOX | jq -r '.data.url')
echo "Inbox URL: \$INBOX_URL"
# 2. Compute an HMAC signature for your test payload
PAYLOAD='{"id":"evt_001","event":"order.created","amount":2999}'
TIMESTAMP=\$(date +%s)
SIGNED_PAYLOAD="\$TIMESTAMP.\$PAYLOAD"
HMAC=\$(curl -s -X POST https://api.botoi.com/v1/hash/hmac \\
-H "Content-Type: application/json" \\
-d "{
\\"text\\": \\"\$SIGNED_PAYLOAD\\",
\\"key\\": \\"test_secret_key\\",
\\"algorithm\\": \\"sha256\\"
}" | jq -r '.data.hmac')
echo "HMAC: \$HMAC"
# 3. Send the signed payload to your inbox
curl -s -X POST "\$INBOX_URL" \\
-H "Content-Type: application/json" \\
-H "X-Webhook-Signature: \$HMAC" \\
-H "X-Webhook-Timestamp: \$TIMESTAMP" \\
-d "\$PAYLOAD"
# 4. Retrieve and inspect the payload
curl -s -X POST "https://api.botoi.com/v1/webhook/inbox/\$INBOX_ID/list" | jq '.data.payloads'
Der Listenendpunkt gibt alle empfangenen Nutzlasten zurück:
[
{
"id": "p_abc123",
"received_at": "2026-04-05T14:30:00Z",
"body": {
"id": "evt_001",
"event": "order.created",
"amount": 2999
}
}
]
Mit diesem Workflow können Sie den vollständigen Signaturfluss über die Befehlszeile testen. Tauschen Sie die Posteingangs-URL gegen Ihre aus Produktionsendpunkt zum Ausführen von Integrationstests in CI.
Wie Stripe, GitHub und Shopify Webhooks signieren
| Anbieterin | Kopfzeile | Algorithmus | Signiertes Nutzlastformat | Zeitstempel enthalten |
|---|---|---|---|---|
| Streifen | Stripe-Signature |
HMAC-SHA256 | t=UNIX.v1=HEX |
Ja |
| GitHub | X-Hub-Signature-256 |
HMAC-SHA256 | sha256=HEX |
NEIN |
| Shopify | X-Shopify-Hmac-Sha256 |
HMAC-SHA256 | Base64-kodiert | NEIN |
| Twilio | X-Twilio-Signature |
HMAC-SHA1 | URL + sortierte Parameter | NEIN |
Stripe ist das einzige Unternehmen, das einen Zeitstempel mit der Signatur bündelt und Ihnen so einen sofort einsatzbereiten Wiedergabeschutz bietet. Fügen Sie für GitHub und Shopify Ihren eigenen Zeitstempel-Header hinzu oder überprüfen Sie die Ereigniserstellungszeit im Payload-Body.
Checkliste vor Produktionsbeginn
- Speichern Sie das Webhook-Geheimnis in einer Umgebungsvariablen, nicht im Quellcode.
- Analysieren Sie den rohen Anforderungstext, nicht die JSON-analysierte Version. HMAC signiert die genauen Bytes auf der Leitung.
- Verwenden
crypto.timingSafeEqualzum Signaturvergleich. Niemals verwenden===. - Geben Sie 200 zurück, bevor Sie die langsame Verarbeitung ausführen. Stellen Sie die Arbeit in die Warteschlange und bestätigen Sie die Lieferung.
- Protokollieren Sie abgelehnte Anfragen (ungültige Signatur, veralteter Zeitstempel, Duplikat) zur Fehlerbehebung und Warnung.
- Legen Sie eine TTL für Ihren Idempotenzspeicher fest. 72 Stunden decken die meisten Wiederholungsfenster des Anbieters ab.
- Testen Sie mit einem Einweg-Posteingang und berechneten HMAC-Signaturen, bevor Sie einen Live-Anbieter verbinden.
FAQ
- Warum HMAC-SHA256 anstelle eines gemeinsamen Geheimnisses in einem Abfrageparameter verwenden?
- Ein Abfrageparameter wird im Klartext durch Proxys und Zugriffsprotokolle übertragen. HMAC-SHA256 signiert den gesamten Anfragetext mit dem Geheimnis, sodass ein Angreifer, der die URL abfängt, trotzdem keine gültige Signatur fälschen kann.
- Wie lange sollte ich verarbeitete Ereignis-IDs für Idempotenz aufbewahren?
- Bewahren Sie sie mindestens 24 bis 72 Stunden lang auf. Die meisten Webhook-Anbieter versuchen es innerhalb dieses Zeitfensters erneut. Nach 72 Stunden können Sie alte Ausweise sicher aus Ihrem Geschäft entfernen.
- Welche Zeitstempeltoleranz sollte ich für den Wiedergabeschutz verwenden?
- Fünf Minuten (300 Sekunden) sind der Standard. Stripe benötigt 300 Sekunden. Bei kürzeren Zeitfenstern besteht die Gefahr, dass legitime Lieferungen abgelehnt werden, die durch eine Netzwerküberlastung verzögert werden.
- Kann ich den Botoi-Hash/Hmac-Endpunkt verwenden, um eingehende Webhooks zu überprüfen?
- Ja. POSTEN Sie den Payload-Body und Ihr Geheimnis mit dem Algorithmus sha256 nach /v1/hash/hmac. Vergleichen Sie den zurückgegebenen HMAC mit dem Signaturheader des Webhook-Anbieters.
- Benötige ich alle drei Schutzmaßnahmen oder kann ich eine auswählen?
- Die HMAC-Verifizierung ist das Minimum. Fügen Sie Idempotenz hinzu, wenn Ihr Webhook-Handler Nebenwirkungen wie Gebühren oder E-Mails auslöst. Fügen Sie einen Wiedergabeschutz hinzu, wenn Ihr System Finanztransaktionen oder sicherheitsrelevante Ereignisse verarbeitet.
Starte mit botoi zu entwickeln
150+ API-Endpunkte für Abfragen, Textverarbeitung, Bildgenerierung und Entwickler-Tools. Kostenloser Tarif, keine Kreditkarte nötig.