Insights · Engineering · 2026-05-21 · 9 Min Lesezeit

Shopify Webhook HMAC in Python verifizieren

Warum jeder ungeprüfte Webhook ein offenes Scheunentor ist und wie du ihn in wenigen Zeilen Python dichtmachst

Shopify sendet zu jedem Webhook eine HMAC-Signatur im Header. Wer sie nicht prüft, lässt jeden gefälschte Bestellungen, Storno-Events oder Kundendaten in seine Pipeline kippen. Dieser Artikel zeigt die korrekte Verifikation mit HMAC-SHA256, Base64 und constant-time compare.

Warum Webhooks ohne Signaturprüfung gefährlich sind

Ein Shopify-Webhook ist nichts weiter als ein HTTP-POST an eine URL, die du angegeben hast. Diese URL ist nicht geheim. Sie steht in deiner App-Konfiguration, sie taucht in Logs auf, und sie ist nach kurzer Zeit kein Geheimnis mehr. Wer sie kennt, kann selbst POST-Requests an deinen Endpoint schicken.

Ohne Prüfung glaubt deine Pipeline jedem dieser Requests. Ein gefälschtes orders/create löst eine Bestellbestätigung aus, ein gefälschtes refunds/create bucht Umsatz zurück, ein gefälschtes customers/data_request kann sensible Daten in Bewegung setzen. Das ist keine Theorie, sondern die direkte Folge davon, externen Input ohne Authentifizierung zu verarbeiten.

Shopify löst das, indem es jeden Webhook signiert. Im Header X-Shopify-Hmac-SHA256 steht eine HMAC-Signatur über den exakten Rohbody, erzeugt mit einem geteilten Geheimnis, das nur du und Shopify kennen. Deine Aufgabe ist simpel: dieselbe Signatur selbst berechnen und vergleichen.

Wie HMAC-SHA256 hier funktioniert

HMAC steht für Hash-based Message Authentication Code. Es kombiniert einen geheimen Schlüssel mit den Nachrichtenbytes und erzeugt daraus über eine Hashfunktion (hier SHA256) einen festen Wert. Der Witz: Ohne den Schlüssel kann niemand eine gültige Signatur für einen veränderten Body erzeugen. Eine reine SHA256-Prüfsumme ohne Schlüssel würde genau das nicht leisten.

Shopify nimmt also den Rohbody deines Webhooks, rechnet HMAC-SHA256 mit dem Shared Secret und kodiert das Ergebnis als Base64. Diesen String legt es in den Header. Du machst exakt dasselbe und vergleichst. Stimmen beide Werte, kam der Request wirklich von Shopify und der Body wurde unterwegs nicht verändert.

Wichtig sind drei Details. Erstens: der rohe Body, keine umformatierte Variante. Zweitens: Base64, nicht Hex. Drittens: der Vergleich muss zeitkonstant sein, dazu gleich mehr.

Die Verifikation in Flask

Hier die minimale, korrekte Variante für Flask. Beachte, dass request.get_data() die rohen Bytes liefert, bevor irgendetwas geparst wird.

import hmac, hashlib, base64, os

SECRET = os.environ["SHOPIFY_WEBHOOK_SECRET"].encode()

Im Handler:

  • raw = request.get_data() holt den Rohbody als Bytes.
  • digest = hmac.new(SECRET, raw, hashlib.sha256).digest() erzeugt den HMAC.
  • computed = base64.b64encode(digest).decode() kodiert ihn als Base64.
  • sent = request.headers.get("X-Shopify-Hmac-SHA256", "") liest die mitgesendete Signatur.
  • if not hmac.compare_digest(computed, sent): abort(401) vergleicht beide zeitkonstant.

Erst nach diesem Check darfst du request.get_json() aufrufen. Vorher ist der Body unvertrauenswürdig. Bei FastAPI ist die Logik identisch, du holst den Body mit await request.body() statt request.get_data().

Constant-time compare: warum kein einfaches Gleichheitszeichen

Der naheliegende Vergleich wäre computed == sent. Das ist falsch, und zwar aus einem subtilen Grund. Ein normaler String-Vergleich bricht beim ersten unterschiedlichen Zeichen ab. Wer viele Requests schickt und die Antwortzeiten misst, kann daraus Stück für Stück ableiten, an welcher Stelle seine geratene Signatur abweicht. Das nennt sich Timing-Angriff.

Python bringt dafür hmac.compare_digest mit. Diese Funktion vergleicht immer die volle Länge, unabhängig davon, wo der erste Unterschied liegt. Die Antwortzeit verrät damit nichts über die Korrektheit einzelner Zeichen. Nutze sie für jeden Vergleich von Signaturen, Tokens oder Hashes, niemals den normalen Operator.

Das klingt nach Mikrooptimierung, ist aber gelebter Standard. Der Mehraufwand ist null, der Sicherheitsgewinn real. Wer Vergleiche von Geheimnissen mit == schreibt, hat eine vermeidbare Lücke eingebaut.

Typische Fehler, die die Prüfung kaputt machen

Der mit Abstand häufigste Fehler ist der umformatierte Body. Sobald ein Middleware-Layer das JSON parst und dein Code aus dem geparsten Objekt wieder einen String baut, stimmt die HMAC nicht mehr. Lösung: immer die Rohbytes verwenden, die du vor jedem Parsen abgreifst.

Zweiter Klassiker: das Secret als String statt als Bytes. hmac.new erwartet Bytes für Schlüssel und Nachricht. Vergiss das .encode() nicht, sonst wirft Python einen TypeError oder, schlimmer, du encodest inkonsistent.

Dritter Fehler: Hex statt Base64. Shopify liefert Base64. Wenn du .hexdigest() nimmst und vergleichst, schlägt die Prüfung immer fehl, obwohl der Request echt ist.

Vierter Fehler: zu spätes Antworten. Shopify erwartet innerhalb von fünf Sekunden eine 200. Mach die HMAC-Prüfung und das Annehmen schnell, lege die eigentliche Verarbeitung in eine Queue. Wer das sauber als Teil einer API-Pipeline aufzieht, trennt Annahme und Verarbeitung von Anfang an und vermeidet Timeouts auf der Shopify-Seite.

Vom Snippet zur belastbaren Pipeline

Die HMAC-Prüfung ist die erste Zeile Verteidigung, nicht die letzte. Eine belastbare Anbindung braucht mehr: Idempotenz, damit ein doppelt zugestellter Webhook nicht doppelt verarbeitet wird (Shopify garantiert at-least-once, keine genau-einmal-Zustellung). Dazu speicherst du die X-Shopify-Webhook-Id und überspringst bereits gesehene IDs.

Ebenso brauchst du strukturiertes Logging, damit du im Fehlerfall siehst, welcher Event-Typ wann kam, und einen Retry-sicheren Verarbeitungspfad. Und du brauchst eine klare Trennung: Annahme prüft und quittiert sofort, ein Worker macht die fachliche Arbeit asynchron.

Genau das baue ich bei adsbird als Festpreis-Modul: eine Person plant, baut und übergibt die fertige Shopify-Anbindung samt Doku, der Code bleibt dein Eigentum. Wenn du eine Webhook-Pipeline brauchst, die Signaturen prüft, Dubletten abfängt und sich später erweitern lässt, findest du die Konditionen unter Preise oder schreib direkt an [email protected].

Häufige Fragen

Bevor du fragst.

Woher bekomme ich das Shared Secret für die HMAC-Prüfung?
Bei App-Webhooks ist es der Client Secret deiner App. Bei manuell im Admin angelegten Webhooks zeigt Shopify dir nach dem Anlegen einen eigenen Signing-Key. Beides gehört in eine Umgebungsvariable, nie in den Code. Wenn du Webhooks als Teil einer größeren API-Pipeline betreibst, lege das Secret im Secret-Store deines Hosters ab.
Was passiert, wenn ich die HMAC-Prüfung weglasse?
Dann kann jeder, der deine Endpoint-URL kennt, beliebige Payloads schicken. Gefälschte order/create-Events lösen Lagerbewegungen aus, gefälschte refund-Events stornieren echte Umsätze. Eine sauber gebaute Shopify-Anbindung prüft jeden eingehenden Request, bevor auch nur eine Zeile Body geparst wird.
Muss ich den Body roh lesen oder reicht das geparste JSON?
Du musst den exakten Rohbody verwenden, also die rohen Bytes vor jedem Parsen. Sobald dein Framework das JSON deserialisiert und neu serialisiert, ändern sich Whitespace und Reihenfolge, und die HMAC stimmt nicht mehr. Lies request.get_data() bei Flask oder await request.body() bei FastAPI.
Wie teste ich die Verifikation lokal?
Berechne mit dem Shared Secret und einem Beispiel-Payload selbst die HMAC und schicke sie als Header an deinen lokalen Endpoint, etwa mit curl oder einem kleinen Skript. So musst du nicht auf echte Shopify-Events warten. Brauchst du Hilfe beim Aufsetzen, schreib an [email protected].

Weiterlesen

Verwandte
Artikel.

Tiefer in angrenzende Themen, Architektur, Tools, Praxis.

Konkret werden

Klingt nach
eurem Projekt?

30 Minuten Gespräch, danach weißt du, ob ein vergleichbares Setup für dich Sinn ergibt, und was es realistisch kostet.

Erstgespräch Weitere Artikel