Bei eingehenden Nachrichten prüfst du den Absender nicht, du glaubst ihm
Wenn deine Pipeline eine API aufruft, bestimmst du das Ziel selbst, sprichst über TLS und weist dich mit einem Token aus. Bei einem Webhook läuft es andersherum: Der Anbieter ruft dich an, und deine Endpoint-URL muss dafür aus dem Internet erreichbar sein. Jeder, der die URL kennt oder errät, kann einen POST mit beliebigem JSON dorthin schicken. Wie ein Event von GitHub, einem Zahlungsdienst oder einem CI-System aussieht, steht in deren öffentlicher Dokumentation. Ein gefälschtes Event lässt sich deshalb leicht nachbauen.
Kritisch wird das, sobald ein Event etwas auslöst: einen Deploy, eine Freischaltung, eine Bestellung als bezahlt. Dann ist deine Endpoint-URL ein Schalter, den jeder bedienen kann, der sie kennt. Und ein Endpoint ohne Prüfung fällt im Normalbetrieb nicht auf, weil er mit echten Nachrichten genauso funktioniert wie einer mit Prüfung. Die Lücke zeigt sich erst, wenn jemand sie nutzt.
Das Prinzip: gemeinsames Secret, HMAC über die Nachricht
Anbieter und Empfänger kennen dasselbe Secret. Der Anbieter berechnet über den Inhalt der Nachricht einen HMAC und schickt ihn als Header mit. Du rechnest auf deiner Seite dasselbe nach und vergleichst. Stimmt das Ergebnis, kennt der Absender das Secret und der Inhalt ist unterwegs nicht verändert worden.
Bei GitHub heißt der Header X-Hub-Signature-256. Der Wert ist ein HMAC-Hexdigest, berechnet aus dem Secret und dem Inhalt der Nachricht, und beginnt immer mit sha256=. Daneben schickt GitHub aus Kompatibilitätsgründen weiterhin X-Hub-Signature (HMAC-SHA1), den die Dokumentation ausdrücklich als Altlast einordnet. Für neue Prüfungen gilt der 256er-Header.
Stripe geht einen Schritt weiter. Der Header Stripe-Signature enthält einen Zeitstempel (t=) und eine oder mehrere Signaturen (v1=). Signiert wird per HMAC mit SHA-256 der Text aus Zeitstempel, einem Punkt und dem Body. Alle Signaturschemata außer v1 sollst du ignorieren, damit dich niemand auf ein schwächeres Verfahren herabstuft. Das Secret ist pro Endpoint eindeutig.
Vier Stellen, an denen die Prüfung leise kaputtgeht
1. Du prüfst den geparsten statt den rohen Body. Beide Anbieter verlangen die unveränderten Bytes der Anfrage. GitHub nennt es den originalen Request-Body und weist darauf hin, dass Payloads Unicode-Zeichen enthalten können und du sie als UTF-8 behandeln sollst. Stripe schreibt, dass jede Manipulation des Rohtexts die Verifizierung fehlschlagen lässt, und warnt ausdrücklich vor Frameworks, die den Body vorher anfassen. Das ist eine typische Stolperfalle: Viele Frameworks parsen JSON, bevor dein Handler läuft, und wer den Body danach wieder zu einem String zusammensetzt, bekommt nicht zwingend dieselben Bytes zurück. Registriere für die Webhook-Route deshalb einen Handler, der den Body roh durchreicht.
2. Du vergleichst mit ==. GitHub sagt es deutlich: nie ein einfacher ==-Operator, sondern ein Vergleich in konstanter Zeit, der bestimmte Timing-Angriffe erschwert. Die Doku nennt Rack::Utils.secure_compare für Ruby, hmac.compare_digest für Python und crypto.timingSafeEqual für Node.js. Stripe fordert dasselbe: einen konstanten Zeit-String-Vergleich.
3. Du lässt alte Nachrichten wieder zu (Replay). Ein Angreifer, der eine gültige Nachricht samt Signatur abfängt, kann sie erneut senden. Die Signatur ist dann weiterhin korrekt. Stripe begegnet dem mit dem signierten Zeitstempel: Ist die Signatur gültig, der Zeitstempel aber zu alt, kann deine Anwendung die Nutzlast ablehnen. Die Bibliotheken von Stripe verwenden dafür standardmäßig eine Toleranz von fünf Minuten, und ein Wert von 0 schaltet die Aktualitätsprüfung vollständig ab. Ein Retry des Anbieters bekommt einen neuen Zeitstempel und eine neue Signatur, wird also nicht fälschlich abgelehnt. Bei GitHub empfiehlt die Dokumentation für dieselbe Aufgabe den Header X-GitHub-Delivery, damit jede Zustellung pro Event eindeutig bleibt. Beachte dabei: Bei einer Redelivery behält der Header seinen ursprünglichen Wert. Wer Delivery-IDs zur Deduplizierung speichert, muss deshalb entscheiden, wie er mit einer gewollten Wiederholung umgeht.
4. Du behandelst das Secret wie einen Konfigurationswert. GitHub verlangt eine zufällige Zeichenfolge mit hoher Entropie, die du an einem sicheren Ort ablegst. Ein Token gehört weder in den Code noch in ein Repository. Ein Secret pro Endpoint begrenzt den Schaden, wenn eines davon abfließt. Stripe erlaubt beim Erneuern des Endpoint-Secrets, das alte bis zu 24 Stunden parallel aktiv zu lassen; in dieser Zeit sendet Stripe mehrere Signaturen, und dein Code muss mit mehreren v1=-Werten umgehen können.
Das Minimum in Node.js
Die Reihenfolge im Handler ist wichtig: erst Rohbody, dann Signatur, dann Parsing, dann Verarbeitung.
import crypto from "node:crypto";
function isValid(rawBody, header, secret) {
const expected =
"sha256=" +
crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(header ?? "");
const b = Buffer.from(expected);
// timingSafeEqual wirft bei ungleicher Länge, daher zuerst die Länge prüfen
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Ob deine Implementierung stimmt, kannst du gegen die Testwerte aus der GitHub-Dokumentation prüfen: Mit dem Secret It's a Secret to Everybody und dem Payload Hello, World! muss der Header sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17 herauskommen. Bei ungültiger Signatur lehnst du die Anfrage ab und verarbeitest nichts.
Was zusätzlich hilft und die Signatur nicht ersetzt
GitHub empfiehlt in den Best Practices weitere Schichten: eine HTTPS-Verbindung (GitHub prüft dabei die SSL-Zertifikate), eine IP-Allowlist mit den Adressen aus dem /meta-Endpunkt und eine Antwort mit 2XX innerhalb von 10 Sekunden. Für aufwendige Arbeit empfiehlt die Doku eine Queue, die den Payload asynchron abarbeitet. Stripe rät ebenfalls, vor jeder komplexen Logik zügig einen 2xx-Statuscode zurückzugeben.
Die IP-Allowlist musst du laut Dokumentation regelmäßig nachziehen, weil GitHub die Adressen ändert. Die Signatur prüft dagegen den Inhalt jeder einzelnen Nachricht und hängt nicht an Netzwerkadressen. Sinnvoll ist deshalb: Signatur als Pflicht, Allowlist und Queue als zusätzliche Schichten.
Fünf Testfälle, bevor der Endpoint live geht
- Unveränderter Body mit gültiger Signatur: wird angenommen.
- Ein Zeichen im Body geändert: wird abgelehnt.
- Signatur-Header fehlt oder ist leer: wird abgelehnt.
- Gültige Signatur mit zu altem Zeitstempel (sofern der Anbieter einen mitsigniert): wird abgelehnt.
- Dieselbe Delivery- oder Event-ID zweimal: wird nur einmal verarbeitet (eine gewollte Redelivery entscheidest du bewusst).
Schlägt einer der fünf Fälle fehl, prüfst du gerade nur, ob ein Header da ist, nicht, ob die Nachricht echt ist.
Wo das Thema an Nachbarthemen grenzt
Die Signaturprüfung beantwortet eine einzige Frage: Ist diese eingehende Nachricht echt? Sie ersetzt keine der übrigen Kontrollen in der Pipeline, sondern ergänzt sie. Wie du verhinderst, dass ein Secret überhaupt im Repository landet, steht in Secret Scanning und Push Protection. Wie du Code von außen vor dem ersten Einsatz prüfst, zeigt GitHub Actions vor dem ersten Einsatz prüfen. Und wer in GitHub Actions welches Secret sehen darf, klärt Secrets-Scoping in GitHub Actions. Ein Webhook-Secret ist ein Secret wie jedes andere. Es hat nur eine Besonderheit: Es schützt eine Nachricht, die von außen zu dir kommt.
Wo Unterstützung ansetzt
Das Leistungsspektrum ist gestaffelt und baut auf Delivery-Transparenz auf. Es läuft über zwei Felder: Delivery- und Nachweiskontrollen in die Pipeline einziehen (Implementation) und Nachvollziehbarkeit dort schaffen, wo KI-gestützte Schritte in Build und Deployment Freigaben und Verantwortung verwischen (AI Governance). Ein festes Angebot daraus entsteht erst, wenn klar ist, was du wirklich brauchst. Kontakt: mm@mhm-dl.de.
Über das Anmeldeformular für den Newsletter trägst du deine E-Mail-Adresse ein, kreuzt die Einwilligung an und bestätigst die Anmeldung danach per Mail. Mit der Anmeldung willigst du ein, dass der Newsletter unter anderem folgende Themen behandelt: Schulungen zu Docker, Kubernetes, CI/CD, Git-Workflows und DevSecOps-Werkzeugen sowie die Anforderungen aus NIS2, CRA und DORA und deren technische Umsetzung.
Hinterlasse einen Kommentar