Wie der Angriff funktioniert
Bei der Einrichtung tauschen Anbieter und Empfänger ein gemeinsames Geheimnis (Secret/Signing Secret) aus. Beim Versand berechnet der Anbieter einen HMAC (üblicherweise HMAC-SHA256) über den exakten, unveränderten Anfragetext und legt das Ergebnis in einen HTTP-Header.
GitHub nutzt den Header "X-Hub-Signature-256" mit dem Format "sha256=" und HMAC-SHA256 über den Rohtext. Stripe nutzt den Header "Stripe-Signature" mit dem Format "t=,v1="; signiert wird die Verkettung aus Zeitstempel und Rohtext (signed_payload), und die Bibliotheksfunktion constructEvent() prüft Signatur und Zeitstempel zusammen.
Der Empfänger berechnet denselben HMAC mit seinem Secret nach und vergleicht ihn mit dem Header-Wert. Drei Punkte sind dabei kritisch:
- Rohtext vergleichen: Es muss der Rohtext verglichen werden, nicht das neu serialisierte/geparste JSON, da sonst der HMAC nicht passt.
- Zeitkonstanter Vergleich: Der Vergleich muss zeitkonstant erfolgen (z.B. secure_compare/timingSafeEqual), um Timing-Angriffe zu vermeiden.
- Replay-Schutz per Zeitstempel: Der mitsignierte Zeitstempel verhindert Replay, indem der Empfänger Anfragen ablehnt, deren Zeitstempel zu weit von der aktuellen Zeit abweicht (tolerance).
Davon getrennt entsteht ein SSRF-Risiko, wenn die Anwendung dem Anbieter erlaubt, beliebige Callback-/Webhook-Ziel-URLs zu konfigurieren oder selbst Daten an benutzergesteuerte URLs nachlädt. Dann kann ein internes Ziel (interne IP, Cloud-Metadaten) angesprochen werden.
Ohne Signatur- und Replay-Prüfung sind eingehende Webhooks fälschbar.
Beispiel
GitHub-Verifikation (offizielles Testvektor-Beispiel aus der GitHub-Doku). Secret: "It's a Secret to Everybody", Payload: "Hello, World!". GitHub liefert die Anfrage mit dem Header: X-Hub-Signature-256: sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17. Schritte beim Empfänger:
- Rohtext des Body unverändert auslesen.
expected = "sha256=" + HMAC_SHA256(secret, rohtext)als Hex berechnen.expectedzeitkonstant gegen den Header-Wert vergleichen (z.B.Rack::Utils.secure_compareoder Nodecrypto.timingSafeEqual).- Bei Übereinstimmung verarbeiten, sonst mit Fehler ablehnen.
Mit den obigen Werten muss exakt "757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17" herauskommen; jede Manipulation am Body oder ein falsches Secret erzeugt eine abweichende Signatur und die Anfrage wird verworfen. (Unabhängig per openssl-HMAC nachgerechnet und exakt bestätigt.)
Auswirkung
Ohne Signaturprüfung kann jeder, der die Endpunkt-URL kennt, gefälschte Ereignisse einspielen und damit kritische Geschäftslogik auslösen - etwa eine Bestellung als bezahlt markieren, Guthaben gutschreiben oder einen Deployment-/CI-Schritt starten.
Ohne Replay-Schutz kann ein einmal abgefangenes, gültig signiertes Ereignis beliebig oft erneut gesendet werden (z.B. wiederholte Gutschriften). Wird zusätzlich die Webhook-/Callback-URL nicht eingeschränkt, ermöglicht SSRF Zugriff auf interne Dienste und Cloud-Metadaten-Endpunkte.
Folgen: finanzieller Betrug, Datenmanipulation, unautorisierte Aktionen und Zugriff auf interne Systeme.
So schützt man sich
- Jede eingehende Webhook-Anfrage mit dem geteilten Secret per HMAC (SHA-256) verifizieren, bevor die Nutzlast verarbeitet wird - GitHub: X-Hub-Signature-256, Stripe: Stripe-Signature.
- Den unveränderten Rohtext (raw body) signieren bzw. prüfen, nicht das geparste/neu serialisierte JSON.
- Den Signaturvergleich zeitkonstant durchführen (z.B. Rack::Utils.secure_compare, Node crypto.timingSafeEqual), nie mit normalem == .
- Replay-Schutz aktivieren: mitsignierten Zeitstempel prüfen und Anfragen außerhalb eines kleinen Toleranzfensters (tolerance) ablehnen; nach Möglichkeit eine Nonce/Event-ID einmalig verarbeiten (Idempotenz).
- Bibliotheken des Anbieters nutzen (z.B. Stripe constructEvent()), die Signatur und Zeitstempel gemeinsam validieren, statt eigene Logik zu bauen.
- Secret mit hoher Entropie zufällig erzeugen, sicher speichern (nicht im Code) und rotierbar halten; Endpunkt nur über HTTPS.
- Fehlt die Signaturprüfung-/der Header, die Anfrage ablehnen statt sie zu verarbeiten.
- SSRF vermeiden: Webhook-/Callback-Ziel-URLs nicht frei zulassen, sondern gegen eine Allowlist erlaubter Domains/IP-Adressen (v4 und v6) prüfen (strikter String-Vergleich); interne IP-Bereiche und Cloud-Metadaten-Endpunkte (z.B. 169.254.169.254) blockieren und DNS-Rebinding berücksichtigen.
Quellen
- Validating webhook deliveries GitHub Docs, 2026
- Receive Stripe events in your webhook endpoint Stripe Docs, 2026
- Verify webhook signatures manually Stripe Docs, 2026
- Server-Side Request Forgery Prevention Cheat Sheet OWASP Cheat Sheet Series, 2026