Warum Webhook-Security kein Detail ist
Ein Webhook ist ein öffentlicher Eingang in dein System. Und das Internet findet ihn. Spätestens dann, wenn jemand:
- zufällig deine URL scannt,
- bewusst Spam schickt,
- Payloads manipuliert,
- oder versucht, Events erneut abzuspielen (Replay).
Deshalb gilt bei Webhooks nicht "wird schon passen", sondern: Vertrauen entsteht erst nach Verifikation.
Zur Übersicht: Workflows: Automatisierung
Ziel dieses Artikels
Du bekommst ein praktisches Setup, das in den meisten Projekten funktioniert:
- Signaturen korrekt prüfen (ohne typische Fallen)
- Replay-Angriffe verhindern
- sichere Defaults für Logging und Fehlerbehandlung
- robuste Verarbeitung ohne Duplikate
Kurzfazit (TL;DR)
- Behandle jeden Webhook als untrusted Input, auch wenn er "vom Provider" kommt.
- Prüfe die Signatur über den rohen Request-Body (nicht über geparstes JSON).
- Implementiere Replay-Schutz mit Timestamp-Toleranz und Event-Dedupe.
- Antworte nur dann mit 2xx, wenn der Webhook verifiziert ist.
- Logge sparsam: keine Secrets, keine Tokens, keine kompletten Payloads.
Bedrohungsmodell in einem Satz
Die wichtigste Annahme ist: Jeder kann deinen Webhook-Endpoint aufrufen.
Deine Aufgabe ist nur: Echte Events erkennen und alles andere sicher verwerfen.
Schritt 1: Signaturprüfung als Pflicht
Die meisten Provider bieten Signaturen, oft als HMAC oder vergleichbar. Das Grundprinzip ist immer gleich:
- Provider sendet Payload + Signatur-Header
- Du berechnest selbst die erwartete Signatur mit einem Secret
- Du vergleichst erwartete Signatur und Header-Signatur
Typische Fehler bei Signaturen
-
JSON parsen und dann signieren Schon kleine Unterschiede in Whitespace oder Key-Reihenfolge brechen die Signatur. Du brauchst den originalen Body als Bytes.
-
String-Vergleich ohne Constant-Time Ein normaler Vergleich kann Timing-Leaks haben. Nutze eine constant-time Compare-Funktion, wenn du selbst implementierst.
-
Mehrere Secrets und falsches wählen Bei Secret-Rotation existieren oft "aktuelles" und "altes" Secret. Prüfe gegen beide, aber halte das Fenster klein und clean.
-
Secret im Code oder in Logs Secrets gehören in eine sichere Secret-Verwaltung (Environment, Vault, Secret Manager), niemals ins Repo und nie in Debug-Logs.
Minimaler Pseudocode
Das Muster in neutraler Form:
rawBody = request.bodyBytes
signature = request.headers["..."]
timestamp = request.headers["..."] (optional)
assert rawBody.size <= maxSize
assert signature exists
assert timestamp within tolerance (if used)
expected = HMAC(secret, timestamp + "." + rawBody)
if !constantTimeEqual(signature, expected):
return 401/403
enqueue(rawBody)
return 200
Wichtig: erst verifizieren, dann verarbeiten.
Schritt 2: Replay-Schutz ist mehr als "Idempotenz"
Ein Replay-Angriff heißt: Jemand nimmt einen echten, signierten Request und schickt ihn später erneut.
Wenn du nur "Signatur ok" prüfst, kann das reichen, um Side-Effects erneut auszulösen (z.B. doppelte E-Mails).
Zwei robuste Bausteine:
Baustein A: Timestamp-Fenster
Viele Provider signieren zusätzlich einen Timestamp. Dann kannst du sagen:
- akzeptiere nur Requests, die z.B. maximal 5 Minuten alt sind
Damit sind alte Replays wertlos.
Baustein B: Event-Dedupe
Fast alle Webhook-Events haben eine Event-ID. Speichere diese IDs mit TTL und verarbeite jede ID nur einmal.
Wenn es keine Event-ID gibt, baue einen stabilen Key aus Event-Typ + Objekt-ID und einem stabilen Zeitfeld.
Wenn du Dedupe sauber bauen willst: Idempotenz in Automationen
Schritt 3: Request-Hygiene und sichere Defaults
Limitieren, bevor du liest
Schütz dich gegen unnötige Last:
- Body-Size-Limit (z.B. 256 KB oder nach Provider)
- Request-Timeouts
- Reject von unerwarteten Content-Types (z.B. nur
application/json)
Das ist nicht "nice to have". Das ist DoS-Hygiene.
Vertraue nicht auf eine "geheime URL"
Eine lange URL ist kein Sicherheitsmechanismus. Sie kann ein zusätzlicher Rauschen-Filter sein, aber ersetzt keine Signatur.
IP-Allowlist nur als Zusatz
IP-Listen können helfen, aber sie sind oft:
- nicht stabil (Änderungen beim Provider)
- schwer zu pflegen
- bei Proxies/CDNs schwer korrekt auszuwerten
Wenn du IPs allowlisten willst, dann als zweite Schicht - nicht als einzige.
Schritt 4: Fehlercodes und Retries bewusst nutzen
Viele Provider retryen Webhooks bei 5xx. Das ist gut, wenn dein System kurz down ist. Es ist schlecht, wenn du systematisch 5xx gibst und damit eine Retry-Flut erzeugst.
Praktische Leitlinien:
- 401/403 bei invaliden Signaturen: keine Retries, sofort verwerfen
- 400 bei invalidem Format: verwerfen (oft permanent)
- 5xx bei temporären Problemen: Provider darf retryen
- 2xx nur, wenn du das Event angenommen hast (nach Verifikation)
Wenn du Retries stabil bauen willst (Backoff, Jitter, 429): Retries, Backoff & Rate Limits
Schritt 5: Verarbeitung entkoppeln
Der Webhook-Endpoint sollte fast immer nur:
- verifizieren
- minimal speichern / enqueue
- schnell 2xx antworten
Warum:
- Provider erwarten oft schnelle Responses
- langsame Verarbeitung erzeugt Timeouts und Duplikate
- du willst Backoff und Concurrency kontrollieren (Worker statt Request-Thread)
Einordnung dazu: Webhook vs. Polling
Schritt 6: Logging ohne Datenleck
Logs sind Gold für Debugging, aber Webhooks sind oft voll mit sensiblen Daten.
Sichere Defaults:
- logge Event-ID, Event-Typ, Timestamp, Provider, Result (accepted/rejected)
- logge keine Tokens, Signaturen, Secrets
- logge Payloads nur redacted und nur bei Bedarf
Wenn du doch Payloads brauchst: Speichere sie verschlüsselt und mit kurzer TTL, statt sie in Logs zu kippen.
Checkliste: Webhook-Security in einem Workflow
- Wird die Signatur über den rohen Body geprüft (Bytes, nicht JSON)?
- Gibt es ein Replay-Fenster (Timestamp) oder Event-Dedupe mit TTL?
- Gibt es ein Body-Size-Limit und Timeouts?
- Werden invalid-signature Requests hart verworfen (401/403)?
- Ist Verarbeitung entkoppelt (Queue/Worker), damit der Endpoint schnell ist?
- Sind Side-Effects idempotent oder dedupliziert?
- Sind Logs frei von Secrets und sensiblen Payloads?
Wenn du 5-7 Punkte mit "ja" beantworten kannst, bist du in der Praxis sehr gut.
Typische Fehler, die ich regelmäßig sehe
-
Signatur wird erst nach JSON-Parsing geprüft Das bricht Signaturen oder macht sie unzuverlässig.
-
Webhook-URL als "Secret" Security by obscurity fällt früher oder später auseinander.
-
Kein Replay-Schutz Dann kann ein echter Request später erneut Side-Effects auslösen.
-
Alles synchron verarbeiten Das führt zu Timeouts, Retries und doppelten Runs.
-
Debug-Logs mit Payloads Das wird irgendwann ein Datenschutz- oder Security-Problem.
FAQ
Brauche ich Signaturen wirklich, wenn ich eine IP-Allowlist habe
Ja. Signaturen sind die robustere Identitätsprüfung. IP-Allowlists sind fehleranfällig und oft schwer zu pflegen.
Was ist besser: 401 oder 403 bei invaliden Webhooks
Beides ist in der Praxis ok. Wichtig ist, dass du invalid-signature Requests nicht mit 2xx bestätigst.
Wie gehe ich mit Secret-Rotation um
Praktischer Ansatz:
- akzeptiere für eine kurze Übergangszeit "altes" und "neues" Secret
- rotate regelmäßig und dokumentiert
- halte Secrets pro Provider/Endpoint getrennt
Transparenz
Dieser Blog kann Affiliate-Links enthalten. Aktuell liegt der Fokus auf Inhalten und Workflows. Wenn später Links ergänzt werden, werden sie klar gekennzeichnet.
Nächster Schritt
- Webhooks sicher testen: Webhook-Endpoints testen
- Retries und Rate Limits sauber bauen: Retries, Backoff & Rate Limits
- Duplikate und Replays abfangen: Idempotenz in Automationen
- Webhooks und Polling einordnen: Webhook vs. Polling
- Der Rahmen für stabile Automationen: Workflow-Automatisierung
- Zur Übersicht: Workflows: Automatisierung