Webhook-Signaturen, erklärt

Eine Webhook-Signatur ist eine kurze Zeichenkette, die ein sendender Dienst jeder Zustellung beifügt — ein HMAC, berechnet über die exakten Bytes der Anfrage mit einem Geheimnis, das nur Sender und Empfänger teilen. Sie existiert, damit der empfangende Server zwei Fragen beantworten kann, bevor er handelt: Stammt das wirklich von dem Dienst, von dem es zu stammen behauptet? Und ist der Body genau so angekommen, wie er gesendet wurde? Dieser Artikel erklärt, wie diese Prüfung tatsächlich funktioniert: was der HMAC abdeckt, warum er gegen den rohen statt gegen einen geparsten Body geprüft werden muss, warum der Vergleich in konstanter Zeit laufen muss, wie Zeitstempel Replay-Angriffe begrenzen und warum jeder Anbieter den Header anders formatiert. Wenn Sie zuerst eine echte Zustellung sehen möchten — Header, Signatur und roher Body zusammen —, lassen Sie einen Sender an den Webhook-Tester senden und sehen Sie sich an, was ankommt.

Warum eine geheime URL keine Authentifizierung ist

Der älteste Webhook-Schutz ist eine nicht erratbare URL: ein langes, zufälliges Pfadsegment, das nur der sendende Dienst kennt. Sie fühlt sich an wie ein Passwort, verhält sich aber wie eine Adresse — und Adressen sickern durch. Die URL steht in Zugriffsprotokollen, Proxy-Protokollen, Monitoring-Dashboards, Screenshots in Fehlerberichten und auf den Konfigurationsseiten des sendenden Dienstes selbst, wo jeder Support-Ingenieur — oder jeder, der eines dieser Systeme kompromittiert — sie lesen kann. Ein Passwort lässt sich gehasht speichern und pro Benutzer rotieren; eine URL wird überall, wo sie verwendet wird, wörtlich kopiert, und jede Kopie ist eine dauerhafte Einladung.

Das tiefere Problem ist, was der Endpoint mit einer Anfrage tut, sobald sie ankommt. Webhook-Handler markieren Rechnungen als bezahlt, legen Konten an, vergeben Lizenzen, löschen Daten. Das sind Aktionen, die es wert sind, gefälscht zu werden, und eine gefälschte Anfrage kostet einen Angreifer ein einziges HTTP-POST von irgendwo aus dem Internet. TLS hilft hier nicht, denn TLS authentifiziert Ihren Server gegenüber demjenigen, der sich verbindet; es sagt nichts darüber, wer die verbindende Partei ist. Eine Signatur verlagert das Vertrauen von „weiß, wo man anklopft“ zu „kennt ein Geheimnis, das in der Anfrage selbst nie auftaucht“ — eine kategorisch stärkere Behauptung.

Was eine HMAC-Signatur tatsächlich beweist

Sender und Empfänger teilen ein Geheimnis, das bei der Registrierung des Endpoints erzeugt wird. Für jede Zustellung berechnet der Sender einen HMAC — einen schlüsselabhängigen Hash — über eine definierte Menge von Bytes: typischerweise den rohen Body der Anfrage, oft mit vorangestelltem Zeitstempel, manchmal mit einer Zustellungs-ID. Das Ergebnis reist in einem Header. Der Empfänger berechnet den HMAC über die empfangenen Bytes mit seiner eigenen Kopie des Geheimnisses neu und vergleicht die beiden Werte. Eine Übereinstimmung beweist zwei Dinge zugleich: Der Sender besaß das Geheimnis, und die abgedeckten Bytes sind bit für bit die Bytes, die der Sender signiert hat. Ändern Sie ein einziges Zeichen im Betragsfeld, und der neu berechnete Wert weicht vollständig ab.

Zwei Grenzen sollte man klar benennen. Erstens ist eine Signatur keine Verschlüsselung: Der Body durchquert das Netz für jeden lesbar, der in der Position ist, ihn zu sehen — ihn privat zu halten ist die Aufgabe von TLS —, und die Signatur bürgt nur für Herkunft und Integrität. Zweitens würde ein einfacher Hash des Bodys nichts beweisen, denn jeder kann von allem einen Hash bilden. Das Geheimnis ist es, das einen Hash in einen Nachrichtenauthentifizierungscode verwandelt. Der Hash-Generator auf dieser Seite berechnet schlüssellose Hashes, die als Prüfsummen taugen — aber eine Prüfsumme, die jeder neu berechnen kann, authentifiziert niemanden.

Prüfen Sie den rohen Body, Byte für Byte

Die häufigste Ursache für „die Prüfung schlägt bei echten Zustellungen fehl“ ist kein Angreifer — es ist das eigene Framework des Empfängers. Die Signatur wurde über den Body genau so berechnet, wie er übertragen wurde: diese Schlüsselreihenfolge, diese Leerzeichen, diese Escapes, diese Zahlenformatierung. Die meisten Web-Frameworks parsen den JSON-Body hilfsbereit in ein Objekt, bevor Ihr Handler läuft, und wenn Sie dieses Objekt zur Prüfung wieder serialisieren, erhalten Sie dieselben Daten in anderen Bytes: Schlüssel umsortiert, Leerzeichen hinzugefügt, 1e3 als 1000 gerendert, Unicode escaped oder nicht. Semantisch identisch, kryptografisch verschieden — und die Prüfung schlägt bei einer Zustellung fehl, die vollkommen echt war. Stripes Dokumentation formuliert die Regel unmissverständlich: Die Signaturprüfung braucht den rohen Body, und das Framework darf ihn nicht verändern.

Die operative Konsequenz ist eine Reihenfolge: Erfassen Sie den rohen Body der Anfrage, bevor irgendetwas ihn parst, prüfen Sie die Signatur gegen diese Bytes, und parsen und verarbeiten Sie erst dann. Proxys und Middleware, die Bodys umschreiben — umkodieren, normalisieren, hübsch formatieren —, brechen Signaturen genauso sicher wie ein Parser, weshalb die Fehlersuche damit beginnt, sich die Anfrage genau so anzusehen, wie sie angekommen ist. Genau diese Ansicht gibt Ihnen ein Empfangswerkzeug: Wenn eine Zustellung im Webhook-Tester landet, inspizieren Sie die unberührte Anfrage, Header und Body wie übertragen, an der sich eine Signatur ehrlich prüfen lässt.

Vergleich in konstanter Zeit — und warum die Uhr Teil der Signatur ist

Der Vergleich der beiden HMAC-Werte sieht aus wie der triviale Schritt und verbirgt ein subtiles Leck. Ein gewöhnlicher Zeichenkettenvergleich stoppt beim ersten abweichenden Zeichen, antwortet also schneller, wenn ein Rateversuch früh falsch liegt, und langsamer, wenn zufällig ein längeres Präfix stimmt. Über viele Anfragen hinweg lässt diese Zeitdifferenz einen Angreifer sich Zeichen für Zeichen an eine gültige Signatur heranmessen, ohne je das Geheimnis zu erfahren. Ein Vergleich in konstanter Zeit untersucht immer die volle Länge und braucht gleich lang, ob die Werte im ersten oder im letzten Zeichen abweichen. Die Plattformdokumentationen sind hier unmissverständlich — GitHubs Dokumentation warnt ausdrücklich vor einem einfachen Gleichheitsoperator und verweist auf Funktionen mit konstanter Laufzeit wie crypto.timingSafeEqual. Verwenden Sie die Bibliotheksfunktion; schreiben Sie die Vergleichsschleife nicht selbst.

Frische ist die dritte Eigenschaft, und eine Signatur allein kann sie Ihnen nicht geben: Eine abgefangene echte Zustellung, wortwörtlich erneut gesendet, trägt eine vollkommen gültige Signatur. Deshalb falten viele Verfahren einen Zeitstempel in die signierten Bytes, und deshalb verwerfen Empfänger Zustellungen, deren Zeitstempel außerhalb eines Toleranzfensters liegt — bei Stripe standardmäßig fünf Minuten, laut dessen Dokumentation. Das Fenster ist ein bewusster Kompromiss: breit genug, um Taktdrift und langsame Warteschlangen zu schlucken, schmal genug, dass eine abgefangene Anfrage schnell veraltet. Innerhalb des Fensters fällt die Verteidigung an die Idempotenz: Notieren Sie die Zustellungs- oder Ereignis-ID von allem, was Sie verarbeitet haben, und verwerfen Sie Wiederholungen — was Sie ohnehin brauchen, denn ehrliche Sender versuchen es erneut, und ein erneuter Versuch ist ein Replay mit guten Absichten.

Warum der Header bei jedem Anbieter anders aussieht

Die Mechanik ist Standard; die Verpackung ist nie konvergiert. Stripe sendet einen Stripe-Signature-Header der Form t=<Zeitstempel>,v1=<Signatur> und kann mehrere v1-Werte zugleich enthalten. GitHub sendet X-Hub-Signature-256, dessen Wert die Zeichenkette mit Algorithmus-Präfix sha256=<Hex-Digest> ist — ohne Zeitstempel, sodass eine GitHub-Zustellung keinen eingebauten Ablauf hat und die Replay-Abwehr vollständig auf der Deduplizierung von Zustellungs-IDs ruht. Die Spezifikation Standard Webhooks ist ein Versuch, die Frage zu klären: getrennte Header für Zustellungs-ID, Zeitstempel und Signatur, wobei die Signatur über alle drei plus den Body berechnet wird, damit neue Anbieter aufhören, inkompatible Varianten zu erfinden. Die Unterschiede sind historische Zufälle, keine verschiedenen Sicherheitsmodelle — in jedem Verfahren ist die Invariante dieselbe: ein gemeinsames Geheimnis, eine definierte Menge abgedeckter Bytes, ein Vergleich in konstanter Zeit.

Die mehreren v1-Werte in Stripes Format existieren für die Operation, vor der jeder Empfänger irgendwann steht: die Rotation des Geheimnisses. Naiv gemacht — Geheimnis ändern, deployen, hoffen — bricht die Rotation alles, was gerade unterwegs ist, denn mit dem alten Geheimnis signierte Wiederholungen und wartende Zustellungen schlagen plötzlich fehl. Das brauchbare Muster ist ein paralleles Fenster: Während der Überschneidung werden Signaturen unter dem alten wie dem neuen Geheimnis akzeptiert, Sie bestätigen, dass Zustellungen unter dem neuen verifizieren, und erst dann wird das alte außer Dienst gestellt. Dasselbe Fenstermuster taucht in der JWT-Welt aus Schlüssel-IDs und überlappenden Schlüsseln auf, und der Fehlermodus, wenn Teams es auslassen, ist identisch, wie der Artikel über JWT-Fehler beschreibt. Ein Geheimnis, das sich nicht ohne Ausfallzeit rotieren lässt, wird nicht rotiert, selbst wenn es sollte — auch an dem Tag, an dem es durchsickert —, und deshalb ist das unscheinbare parallele Fenster Teil des Protokolls und kein nachträglicher Einfall.

Quellen und weiterführende Lektüre

Häufig gestellte Fragen

Die Prüfung schlägt bei Zustellungen fehl, die mit Sicherheit echt sind. Wo schaue ich zuerst hin?

Auf den Body. Prüfen Sie der Reihe nach: Verifizieren Sie die rohen Bytes, wie sie ankamen, oder eine geparste und wieder serialisierte Version? Verwenden Sie das Geheimnis für diesen Endpoint und diesen Modus — Test- und Live-Endpoints haben verschiedene Geheimnisse, und jeder registrierte Endpoint hat sein eigenes? Schreibt irgendetwas zwischen Sender und Ihrem Handler — ein Proxy, eine Framework-Middleware — den Body um? Eine Zustellung unberührt abzufangen, etwa im Webhook-Tester, und gegen exakt diese Bytes zu prüfen, trennt in einem Schritt „der Sender signiert merkwürdig“ von „mein Stack verändert die Bytes“.

Wir nutzen bereits HTTPS. Ist die Signatur dann nicht überflüssig?

Nein — die beiden authentifizieren in verschiedene Richtungen. TLS beweist dem Sender, dass er Ihren echten Server erreicht hat, und verschlüsselt den Body auf dem Weg. Es stellt keinerlei Anforderung daran, wer sich verbindet: Jeder im Internet kann eine vollkommen gültige TLS-Verbindung zu Ihrem Endpoint öffnen und einen gefälschten „Zahlung erfolgreich“-Body per POST senden. Die Signatur ist der Teil, der Sender und Inhalt authentifiziert. TLS hält eine Zustellung privat; die Signatur macht sie vertrauenswürdig.

Wie oft sollte ein Webhook-Geheimnis rotiert werden?

Sofort, bei jedem Verdacht, dass es durchgesickert ist — es liegt in Dashboards, Umgebungsdateien und CI-Variablen, also behandeln Sie Exposition als Frage der Zeit. Routinemäßig in dem Rhythmus, in dem Ihr Team vergleichbare Zugangsdaten rotiert, vorausgesetzt, die Rotation ist billig: Mit einem Fenster doppelter Akzeptanz ist sie eine Konfigurationsänderung, kein Vorfall. Wenn das Verfahren eines Anbieters die Rotation schmerzhaft macht, wägen Sie das bei der Auswahl von Integrationen mit ab und bevorzugen Sie veröffentlichte Verfahren wie Standard Webhooks — die Fähigkeit zu rotieren ist eine Sicherheitseigenschaft des Protokolls, keine Nettigkeit.