Signiert, Versiegelt, Verdreht: Warum Webhook-Signaturen hinter einem Tunnel scheitern
Webhook-Signatur scheitert hinter einem Tunnel? Rohdaten, JSON-Re-Serialisierung, Zeitstempel-Toleranz und Twilio-URL-Signierung brechen HMAC-Prüfungen. So beheben Sie es.

Quick answer
Webhook-Signaturen scheitern hinter einem Tunnel? So beheben Sie es: quick comparison answer
Choose the tunnel tool based on the network model: public HTTPS URLs for webhooks and demos, private mesh access for internal apps, and managed infrastructure when policy controls matter most.
Which tunnel tool is best for public webhook testing?
Use a public HTTPS localhost tunnel with stable URLs. InstaTunnel focuses on webhook testing, demos, OAuth callbacks, and MCP endpoint workflows.
When should I choose a private network tool instead?
Choose a private mesh or Zero Trust tool when every user and service should stay inside a controlled private network.
Du richtest einen Tunnel auf localhost:3000 ein, sendest ein Testereignis, und dein Handler antwortet 400 Invalid signature. Die Nutzlast sieht gut aus. Das Secret scheint korrekt zu sein. Warum schlägt die Verifizierung der Webhook-Signatur nur dann fehl, wenn die Anfrage durch einen Tunnel oder Reverse-Proxy geleitet wird?
Eine Signatur ist ein Versprechen über exakte Bytes, exaktes Timing und, für einige Anbieter, eine exakte URL. Ein Tunnel, Proxy oder hilfreiche Middleware kann eines davon ändern, während die Anfrage noch intakt aussieht. Dieser Leitfaden behandelt die drei häufigsten Ausfallarten, mit denen Entwickler am häufigsten konfrontiert werden: roher Body vs. neu serialisiertes JSON, Zeitstempel-Toleranz und Twilio-ähnliche Signierung der vollständigen URL. Er endet mit einer Debugging-Checkliste und einem Blick auf die Debugging-Tools, Stand November 2026.
Wie Webhook-Signaturen funktionieren
Beinahe jeder Anbieter verwendet einen HMAC: Der Absender kombiniert ein geteiltes Secret mit einem „signierten Inhalt“, hasht ihn und setzt das Ergebnis in einen Header. Der Empfänger rekonstruiert denselben Inhalt, berechnet seinen eigenen HMAC und vergleicht die beiden in konstanter Zeit. Anbieter unterscheiden sich darin, was in den signierten Inhalt eingeht, sowie im Hash-Algorithmus und der Kodierung.
| Anbieter | Header | Signierter Inhalt | Algorithmus |
|---|---|---|---|
| Stripe | Stripe-Signature (t=…,v1=…) |
{timestamp}.{raw body} |
HMAC-SHA256, hex |
| GitHub | X-Hub-Signature-256 (sha256=…) |
raw payload only | HMAC-SHA256, hex |
| Slack | X-Slack-Signature (v0=…) |
v0:{timestamp}:{raw body} |
HMAC-SHA256, hex |
| Standard Webhooks (Svix, Resend, Clerk, Polar, OpenAI and others) | webhook-signature (v1,…) |
{webhook-id}.{webhook-timestamp}.{raw body} |
HMAC-SHA256, Base64 |
| Twilio | X-Twilio-Signature |
full URL + sorted POST params | HMAC-SHA1, Base64 |
Stripe, Slack und Standard Webhooks binden einen Zeitstempel in die Signatur ein. GitHub signiert den rohen Body ohne Zeitstempel. Twilio signiert die URL. Jedes Design gibt einem Tunnel eine andere Art, Dinge zu beeinträchtigen.
Ausfall #1: Roher Body vs. neu serialisiertes JSON
Der häufigste Grund für eine „Signatur-Abweichung“ ist kein falsches Secret. Es ist ein JSON-Parser, der vor Ihrem Verifizierungscode läuft.
Stripe signiert den rohen, unveränderten Request-Body, und die meisten Web-Frameworks parsen den Body und verwerfen die Originalbytes. Slack-Dokumentation macht denselben Punkt: Die Rohform des Bodys, Whitespace inklusive, wird von Slack verwendet, um die Signatur zu berechnen.
Wie „verfälscht“ aussieht
Wenn Sie JSON parsen und dann JSON.stringify(req.body) aufrufen, um den HMAC zu füttern, validieren Sie eine andere Zeichenkette als die, die der Anbieter signiert hat. Die Re-Serialisierung kann Folgendes ändern:
- Whitespace und Schlüsselreihenfolge, die selten Byte für Byte dem Original entsprechen.
- Unicode-Escaping: Ein Absender kann
"\u00e9"schreiben, während Ihr Serializer ein literals"é"ausgibt. Beide haben denselben Wert, aber unterschiedliche Bytes. - Zahlenformatierung: In JavaScript wird
1.0zu1, und Ganzzahlen jenseits von 2^53 verlieren an Genauigkeit. - Zeichenkodierung: GitHub empfiehlt, Payloads als UTF-8 zu behandeln, da sie Unicode enthalten können.
Jedes davon ergibt ein gültig aussehendes Objekt, aber eine ungültige Signatur.
Die Express-Falle
In Express entscheidet die Reihenfolge der Middleware über alles, weil derjenige Body-Parser, der zuerst läuft, den Request-Stream konsumiert. Das ist fehlerhaft:
app.use(express.json()); // runs first; req.body is now an object
app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), handler);
Die route-bezogene Raw-Parser erhält nichts. Registrieren Sie Webhook-Routen mit express.raw() vor dem globalen JSON-Parser oder erfassen Sie die Bytes mit dem verify-Hook:
app.use(express.json({
verify: (req, _res, buf) => { req.rawBody = buf; } // keep the exact bytes
}));
Hash req.rawBody, niemals JSON.stringify(req.body). Ein Buffer.isBuffer(req.body)-Wächter in einer Raw-Body-Route macht eine irreführende „Signatur-Mismatch“ zu einem offensichtlichen Konfigurationsfehler.
Wo der Tunnel ins Spiel kommt
Ein Tunnel, der nur HTTP-Anfragen weiterleitet, sollte dieselben Body-Bytes liefern, die er erhalten hat. Probleme beginnen, wenn etwas im Pfad die Anfrage verändert: ein Proxy, der den Body neu schreibt, eine Middleware, die ihn normalisiert, oder eine Inspektions-Schicht, die ihn neu codiert. Wenn die Verifizierung passt, wenn Sie die App direkt aufrufen, aber durch den Tunnel fehlschlägt, vergleichen Sie die rohen Bytes, die Ihre App erhält, mit dem, was der Provider gesendet hat.
Tunnel-Inspektoren helfen hier, mit einer Vorsicht. ngrok’s Traffic Inspector sammelt standardmäßig nur Anforderungs-Metadaten, um Secrets nicht offenzulegen. Sie müssen die vollständige Erfassung in Ihrem Konto aktivieren, bevor es vollständige Request- und Response-Bodys protokolliert.
Noch eine Falle bei der lokalen Entwicklung: das Signierungsgeheimnis hängt davon ab, woher das Ereignis stammt. Stripes Troubleshooting-Guide sagt, dass der häufigste Fehler die Verwendung des falschen Endpoint-Secrets ist. Sowohl der Dashboard-Endpunkt als auch die Stripe CLI erzeugen Secrets, die mit whsec_ beginnen, aber sie sind unterschiedliche Secrets. Verifizieren Sie keine CLI-forwarded-Ereignisse mit dem Secret eines Dashboard-Endpunkts, oder umgekehrt. Weder eine ausgefeilte Body-Verarbeitung noch sonstige Tricks können das richten.
Ausfall #2: Zeitstempel-Toleranz
Zeitstempel-signierte Signaturen dienen der Verhinderung von Replay-Angriffen. Ein Angreifer, der eine gültige Anfrage abfängt, kann sie mit ihrer Signatur unverändert erneut senden, weshalb Empfänger auch prüfen, ob der Zeitstempel aktuell ist. Die gängige Default-Einstellung beträgt fünf Minuten:
- Stripe’s Bibliotheken lehnen standardmäßig Ereignisse ab, deren Zeitstempel mehr als 300 Sekunden abweicht, und erlauben es dir, die Toleranz zu überschreiben.
- Slack sagt dir, Anfragen abzulehnen, deren
X-Slack-Request-Timestampmehr als fünf Minuten von der lokalen Zeit abweichen. - Standard Webhooks-Bibliotheken verwenden standardmäßig eine Toleranz von 300 Sekunden. Einige Implementierungen prüfen in beide Richtungen und lehnen zukünftige Zeitstempel sowie veraltete ab.
Für Standard Webhooks bleibt webhook-id über Wiederholungen desselben Ereignisses hinweg gleich, während webhook-timestamp festhält, wann jeder Lieferungsversuch signiert wurde. Der Zeitstempel ist daher bei jedem provider-seitigen Retry aktuell.
GitHub ist die Ausnahme. Die Signatur trägt keinen Zeitstempel, daher gibt es keine Frischeprüfung. Deduplizieren Sie stattdessen anhand der X-GitHub-Delivery-ID.
Warum Tunnel Zeitstempel-Fehler wahrscheinlicher machen
- Uhrdrift auf Entwicklungsrechnern. Ein Laptop, der über Nacht in Schlaf ging, oder eine VM bzw. Container mit einer schiefen Uhr, kann Minuten abweichen. Außerhalb der Toleranz scheitert jede Anfrage, obwohl die Signatur korrekt ist. Synchronisieren Sie zuerst die Uhr.
- Wiederholtes Abspielen erfasster Events. Tunnel-Inspektoren ermöglichen Ihnen, eine abgefangene Anfrage erneut zu senden, auch mit Änderungen. ngrok bietet sowohl eine einfache Wiedergabe als auch eine „Wiedergabe mit Modifikationen“. Ein Stunden-altes Stripe-Ereignis wird die Toleranzprüfung nicht bestehen. Signieren Sie neu mit einem frischen Zeitstempel oder erweitern Sie die Toleranz in einer rein Entwicklungsumgebung.
- Lieferung unterwegs verzögert. Provider-Retries werden erneut signiert, daher sind sie nicht das Problem. Eine einzelne Zustellung, die durch Ihren Tunnel verzögert wird, in die Warteschlange gestellt wird oder das Toleranzfenster überschreitet, ist das Problem.
In der Produktion belassen Sie die 300-Sekunden-Toleranz und deaktivieren die Prüfung außerhalb eines Test-Setups niemals. Pro Ablehnung protokollieren Sie die berechnete Uhrabweichung, damit „veralteter Zeitstempel“ und „ungültige Signatur“ als separate Fehler erscheinen. Slack’s eigene Anweisungen behandeln die Zeitstempelprüfung als eigenständigen Schritt vor dem Signaturvergleich.
Eine Standard Webhooks-Falle
Der Standard Webhooks-Signatur-Schlüssel besteht normalerweise aus dem Secret ohne das Präfix whsec_ und dem Rest, der Base64-dekodiert ist. Nicht jeder Anbieter folgt dem wörtlich. Polar’s Dokumentation bemerkt, dass Polar den vollständigen Secret-String, einschließlich des Präfix, als rohen HMAC-Schlüssel verwendet. Wenn Sie die Verifikation selbst durchführen statt eine Bibliothek zu verwenden, prüfen Sie die Dokumentation Ihres Anbieters.
Ausfall #3: Twilio signiert die vollständige URL
Twilio’s Schema überrascht die Leute am meisten, weil der Körper nur einen Teil dessen darstellt, was signiert wird. Laut Twilio’s Dokumentation:
- Beginnen Sie mit der vollständigen URL der Anfrage, vom Protokoll bis zum Ende der Abfragezeichenfolge.
- Für eine Form-URL-kodierte POST-Anfrage sortieren Sie die POST-Parameter alphabetisch nach dem Namen und hängen jeden Namen und Wert an die URL an, ohne Trennzeichen.
- Berechnen Sie ein HMAC-SHA1 dieses Strings, mit Ihrem Konto Auth Token als Schlüssel.
- Base64-kodieren Sie das Ergebnis und vergleichen Sie es mit
X-Twilio-Signature.
Da die URL eingebaut ist, bricht ein Proxy, der das Schema, den Host oder den Pfad neu schreibt, die Validierung. Die Anfrage ist echt, aber Ihr Code baut eine Zeichenkette wieder auf, die von Twilio gehasht wurde.
Twilio’s Validierungs-Helfer erwarten außerdem, dass Sie alle empfangenen Parameter übergeben, nicht eine fest codierte Liste, denn Twilio kann jederzeit Parameter hinzufügen. Abfragezeichenfolge-Parameter sind bereits Teil der URL, geben Sie sie daher auch nicht separat weiter.
Was die URL hinter einem Tunnel bricht
Twilio’s Support-Richtlinien sagen, der Hauptgrund für 403s hinter einem Proxy sei fast immer eine Diskrepanz zwischen der URL, die Twilio signierte, und der, die Ihre App rekonstruiert. Typische Verdächtige:
- Falsches Schema. TLS endet am Edge, Ihre App sieht
http://, während Twiliohttps://signierte. Stellen Sie das ursprüngliche Schema mitX-Forwarded-Protowieder her, wie Twilio’s Node.js SSL-Termination Guidance erklärt. - Falscher Host. Ihre App sieht einen internen Hostnamen oder
localhost:3000, aber Twilio signierte den öffentlichen Tunnel-Hostnamen. Wenn etwas im Pfad denHost-Header neu schreibt, verwenden Sie ihn nicht, um die signierte URL neu aufzubauen. - Fehlender Pfad oder Abfragezeichenfolge. Framework-Objekte wie
request.urloderrequest.hostkönnen eine teilweise URL zurückgeben, und das erneute Aufbauen vonhttps://hostohne den Pfad ändert die signierte Zeichenkette. - Trailing slash. Der signierte Wert ist eine einfache Zeichenkette, daher erzeugen
/webhookund/webhook/unterschiedliche Signaturen. Stimmen Sie die URL in der Twilio Console exakt ab. - Ports. Der twilio-ruby-Validator prüft die Signatur sowohl mit als auch ohne expliziten Port, mit einem Kommentar, der darauf hinweist, dass die Signatur-Generierung auf Twilio’s Backend uneinheitlich ist. Wenn Sie die Validierung per Hand durchführen, testen Sie beide Formen.
- Ein sich ändernder Hostname. Wenn Ihr Tunnel bei jeder Sitzung einen neuen zufälligen Hostnamen zuweist, aktualisieren Sie die URL sowohl in Twilio als auch in Ihrem Validator. Ein veralteter Wert führt zu jeder Anfrage zum Scheitern.
JSON-Körper und bodySHA256
Nicht jeder Twilio-Webhook ist form-encoded. Für application/json-Anfragen hängt Twilio dem Webhook-URL einen bodySHA256-Abfrageparameter an, der einen SHA-256-Hash des rohen Bodys enthält. Die Twilio-SDKs führen dann zwei Checks durch: Der Hash des Bodys muss mit dem Parameter übereinstimmen, und die Signatur, die über die URL einschließlich dieses Parameters berechnet wird, muss gültig sein. Ein Body-Manipulations-Middleware scheitert daher am Hash-Check, selbst wenn Ihre URL-Logik perfekt ist. In Node ist dies Aufgabe von validateRequestWithBody. Bewahren Sie auch hier den rohen Body auf.
Die sicherste Lösung
- Vertrauen Forwarding-Headern nur von einem Proxy, den Sie kontrollieren. Bauen Sie die URL aus
X-Forwarded-ProtoundX-Forwarded-Hostneu. Das funktioniert nur, wenn ein vertrauenswürdiger Proxy sie tatsächlich setzt; Sie sollten sie aus dem offenen Internet niemals vertrauen. In Express bedeutet das außerdem,trust proxykorrekt zu konfigurieren, damitreq.protocoldem weitergeleiteten Schema entspricht. - Hardcode die öffentliche URL. Sie wissen bereits, was Sie in Twilio konfiguriert haben. Validieren Sie gegen diese Konstante. Das ist langweilig, und es entfernt die ganze Klasse von Rekonstruktionsfehlern.
Twilio’s Schema hat keinen Zeitstempel, daher gibt es kein integriertes Replay-Fenster. Falls Replays relevant sind, deduplizieren Sie anhand von Identifikatoren wie MessageSid oder CallSid.
Eine zehnminütige Debugging-Checkliste
Gehen Sie von der billigsten Prüfung zur teuersten:
- Richtiges Secret, richtige Art? Stripe CLI versus Dashboard Secret, Twilio Auth Token (der Signaturschlüssel) vs. ein API Key, ein rotiertes Secret, das noch nicht propagiert wurde. GitHub lässt
X-Hub-Signature-256ganz fehlen, wenn kein Secret konfiguriert ist. - Richtiger Header? GitHub empfiehlt
X-Hub-Signature-256. Der ältereX-Hub-Signatureverwendet SHA-1 und wird aus Kompatibilitätsgründen behalten. Stripe-Testereignisse können einen älterenv0-Eintrag im Header tragen, prüfen Sie daherv1und ignorieren Sie andere Schemen. - Rohbytes? Protokollieren Sie die Länge des Bodys, den Sie hashen, und vergleichen Sie ihn mit der
Content-Length, die der Tunnel gesehen hat. - Uhr innerhalb der Toleranz? Drucken Sie
now - timestampbei jeder Ablehnung. - Twilio: Welche URL verwenden Sie zum Hashing? Protokollieren Sie sie und vergleichen Sie Zeichen für Zeichen mit der Console. Versuchen Sie es mit und ohne Port und mit abschließendem Slash.
- Funktioniert es, wenn es direkt an die App gesendet wird? Wenn ja, liegt das Problem irgendwo zwischen dem Provider und Ihrer App. Wenn nein, liegt der Fehler in Ihrem Verifizierungs-Code.
- Konstanter Zeitvergleich? Verwenden Sie
crypto.timingSafeEqualoderhmac.compare_digest. Es behebt keinen Mismatch, aber GitHub’s Docs empfehlen es und es verhindert Timing-Lecks beim Debuggen.
Werkzeuge, die man gegen Ende 2026 kennen sollte
- Stripe CLI.
stripe listenleitet Ereignisse zu localhost weiter und zeigt sein eigenes temporäres Signing Secret an, daher ist keine öffentliche URL nötig. Verwenden Sie dieses Secret lokal. - ngrok. Its Traffic Inspector kann aufgezeichnete Anfragen anzeigen und wieder abspielen, und seine Provider-Verifikations-Funktion kann Webhook-Signaturen für unterstützte Provider (Stripe und Mux, unter anderem) prüfen und ungültige Anfragen blockieren, bevor sie Ihre App erreichen. ngrok’s Mux-Leitfaden nennt ein Limit von 500 Validierungen pro Monat bei kostenlosen Konten.
@teamshift/webhook-inspect. Ein aktuelles Open-Source npm-Paket, das Stripe, GitHub, Shopify, Slack, Twilio und generische HMAC-Signaturen verifiziert und erklärt, warum eine Prüfung fehlschlägt. Sein README beschreibt drei Befehle:verifyprüft eine einzelne Anfrage.listenerfasst Webhooks lokal, mit Verifikation.replaysendet eine gespeicherte Aufnahme an einen lokalen Server. Wenn die Verifikation fehlschlägt, versucht das Tool konkrete Fixes und markiert die bestätigte Ursache.
Zu beachten bei webhook-inspect: Socket’s Listing zeigt eine einzige Version (0.1.0) und einen einzelnen Maintainer. Die README enthält eine Beispielaufnahme vom 4. Oktober 2026, aber ich konnte das genaue Veröffentlichungsdatum nicht bestätigen. Betrachten Sie es als Software in der Frühphase und prüfen Sie es, bevor Sie echte Secrets darüber routen.
Ganz gleich welches Tool Sie verwenden, die Technik ist identisch: Reproduzieren Sie den vom Anbieter signierten String und ändern Sie eine Variable nach der anderen, bis die HMACs übereinstimmen.
Produktionshärtung
- Vor dem Parsen verifizieren. Authentifizieren Sie zuerst, deserialisieren Sie danach.
- Getrennte interne Fehler für fehlende Header, veralteten Zeitstempel und ungültige Signatur, während dem Aufrufer eine generische Antwort zurückgegeben wird.
- Unterstützen Sie Secret Rotation. Stripe kann mehrere
v1Signaturen senden, während zwei Secrets aktiv sind, und Standard Webhooks erlaubt mehrere durch Leerzeichen getrennte Signaturen, daher akzeptieren Sie jede gültige Übereinstimmung. - Deduplizieren anhand von
X-GitHub-Delivery, Stripe’s Event-ID,webhook-idoder Twilio SIDs, da Wiederholungen normal sind. - Verwenden Sie in der Entwicklung eine stabile öffentliche URL. Für Twilio-ähnliche URL-Signierung sorgt ein Tunnel-Hostname, der sich zwischen Sitzungen nicht ändert, damit Ihre Provider-Konsole und Ihr Validator synchron bleiben.
- Veröffentlichen Sie nie einen Flag zum Überspringen der Verifikation.
Häufig gestellte Fragen
Warum verifiziert sich meine Signatur lokal, schlägt aber hinter einem Tunnel fehl?
In der Regel sind die Body-Bytes (durch einen Parser oder Proxy verändert), die Uhr (Drift oder replayed Event) oder die URL (ein anderes Schema, Host oder Pfad als der signierte). Klären Sie außerdem das falsche Secret aus, insbesondere Stripe CLI vs. Dashboard. Prüfen Sie die Rohbytes, den Skew und die rekonstruierte URL in dieser Reihenfolge.
Enthält Twilio’s Signatur einen Zeitstempel?
Nein. Twilio signiert die vollständige URL plus sortierte POST-Parameter (oder verwendet bodySHA256 für JSON-Körper). Stripe, Slack und Standard Webhooks enthalten Zeitstempel.
Was ist die richtige Zeitstempel-Toleranz?
Fünf Minuten (300 Sekunden) sind der weit verbreitete Standard in den Bibliotheken von Stripe und Standard Webhooks, und es ist der Zeitraum, den Slack dokumentiert. Erweitern Sie ihn nur im lokalen Test.
Kann ich eine Signatur verifizieren, nachdem das JSON geparst wurde?
Nicht zuverlässig. Neu serialisiertes JSON kann sich in Whitespace, Schlüsselreihenfolge, Escaping und Zahlenformatierung unterscheiden. Erfassen Sie die rohen Bytes, bevor irgendein Parser läuft.
Warum schlägt die Twilio-Validierung fehl, wenn mein Host korrekt aussieht?
Überprüfen Sie Schema, Port, Pfad, Abfragezeichenfolge und den abschließenden Slash. Ein Zeichen ändert die HMAC. Im Zweifel verwenden Sie die URL aus der Twilio Console fest.
Fazit
Eine Webhook-Signatur ist eine kleine Vereinbarung: diese exakt gleichen Bytes, zu dieser Zeit, an diese Adresse. Tunnels und Proxys sind nicht böswillig, aber sie sitzen in der Mitte dieses Vertrags und können jede Klausel stören. Bewahren Sie den rohen Body, halten Sie Ihre Uhr ehrlich, rekonstruieren oder hardcode die exakte URL und verwenden Sie ein Replay- und Diagnose-Tool, wenn ein Abweichen sich nicht durch Starren in Logs lösen lässt. Tun Sie das, und „signed, sealed, mangled“ geht wieder zu „signed, sealed, delivered.“
Quellen
- Stripe: Resolve webhook signature verification errors
- LatticeStripe webhook documentation (raw body requirement and 300-second tolerance)
- GitHub Docs: Validating webhook deliveries
- GitHub Docs (Enterprise Server 3.16): Validating webhook deliveries
- Slack: Verifying requests from Slack
- OpenReplay: How to Verify a Webhook Signature
- Svix: Webhook signature glossary (Standard Webhooks)
- Hookdeck: Guide to OpenAI webhooks (Standard Webhooks adopters)
- Polar Express (Elixir) webhook documentation
- Glean: Webhook delivery (Standard Webhooks tolerance and signing key)
- Whatnot: Webhooks (webhook-id and webhook-timestamp behavior on retries)
- Twilio: Security and validating requests
- Twilio: Webhooks security (bodySHA256)
- Twilio Support: Fixing Request Validation 403 Errors Behind a Proxy
- Twilio: Handle SSL Termination when Validating Webhook Requests in Node.js
- twilio-ruby RequestValidator documentation
- ngrok: Stripe webhooks integration
- ngrok: Microsoft Teams webhooks (Traffic Inspector capture settings)
- ngrok: Mux webhooks (signature validation limits)
- Socket: @teamshift/webhook-inspect
Related InstaTunnel pages
Continue from this article into the most relevant product guides and workflows.
Related Topics
Keep building with InstaTunnel
Read the docs for implementation details or compare plans before you ship.