Webhooks

So sichern Sie Krypto-Zahlungs-Webhooks ab

Was den Endpunkt eines Zahlungs-Webhooks schützt: Prüfung der Webhook-Signatur, idempotente Verarbeitung, exakte Statuscodes, Wiederholungen und Protokollierung. Basierend darauf, wie Qvard-Webhooks tatsächlich funktionieren.

Der Webhook ist der Moment, in dem Ihr Shop entscheidet, dass eine Bestellung bezahlt ist. Wer diesen Endpunkt mit dem richtigen Body aufrufen kann, kann versuchen, Waren umsonst zu bekommen; er verdient also mehr Sorgfalt als die meisten Routen. Dieser Leitfaden beschreibt, was einen Zahlungs-Webhook schützt, basierend darauf, wie Qvard-Webhooks heute tatsächlich funktionieren, einschließlich des Headers X-Qvard-Signature, mit dem sich prüfen lässt, dass eine Benachrichtigung von Qvard stammt.

Was ein Qvard-Webhook ist und was nicht

Wenn eine Zahlung abgeschlossen ist, sendet Qvard eine POST-Anfrage an die in Ihrem Projekt gespeicherte Callback-URL. Der Body ist klein: die id, die Sie beim Erstellen der Zahlung angegeben haben, und der status der Zahlung. Der Webhook wird gesendet, wenn die Zahlung abgeschlossen ist, das Signal, die Bestellung als bezahlt zu markieren.

Die Anfrage trägt einen Header X-Qvard-Signature im Format t=<unix>,v1=<hex>: ein HMAC-SHA256 aus Zeitstempel und rohem Body, berechnet mit Ihrem geheimen Schlüssel. Nach einer Schlüsselrotation werden 24 Stunden lang zwei v1-Werte gesendet, und jeder der beiden ist gültig. Händler mit altem Schlüssel (ohne Präfix qvard_sk_) erhalten im Webhook zusätzlich den Header x-api-key; prüfen Sie die Signatur trotzdem. Prüfen Sie sie über den rohen Body, bevor Sie das JSON parsen. Dieser Leitfaden beschreibt die Signaturprüfung, so wie Qvard sie heute sendet.

Zwei Eigenschaften des Designs bestimmen alles Weitere: Ihr Endpunkt ist eine öffentliche URL, und das einzige Geheimnis, das ihn schützt, ist Ihr geheimer Schlüssel, mit dem die Signatur geprüft wird.

Bedrohungen, die wirklich zählen

Eine kurze Liste ist nützlicher als eine lange:

  • Eine gefälschte Anfrage. Wer Ihren Endpunkt findet, sendet { "id": "1234", "status": "completed" }, damit eine Bestellung als bezahlt markiert wird. Die Signaturprüfung stoppt das.
  • Ein geleakter Schlüssel. Derselbe geheime Schlüssel signiert den Webhook und authentifiziert Ihre API-Aufrufe. Taucht er in einem Repository, einem Log oder Frontend-Code auf, sind beide offen.
  • Eine wiederholte Zustellung. Kein Angriff, aber die häufigste Ursache echter Schäden: Dasselbe Ereignis kommt zweimal an und die Bestellung wird zweimal erfüllt.
  • Ein langsamer oder ausfallender Endpunkt. Verpasste Zustellungen lassen bezahlte Bestellungen unerfüllt.
  • Nachlässiges Logging. Wird die ganze Anfrage protokolliert, landet der Schlüssel in Ihren Logdateien.

Jeder der folgenden Abschnitte schließt eine dieser Bedrohungen.

Prüfen Sie den Schlüssel und vergleichen Sie sicher

Lesen Sie bei jeder Anfrage den Header X-Qvard-Signature und prüfen Sie ihn über den rohen Body, bevor Sie irgendetwas anderes tun. Zwei Details zählen:

  • Vergleich in konstanter Zeit. Ein einfaches === kann verraten, wie viele führende Zeichen übereinstimmten. Verwenden Sie die Konstantzeit-Funktion Ihrer Sprache (crypto.timingSafeEqual in Node.js, hash_equals in PHP, hmac.compare_digest in Python). Prüfen Sie zuerst die Längen, wenn die Funktion gleiche Größen verlangt.
  • Lehnen Sie still ab. Antworten Sie mit 400 ohne Erklärung. Sagen Sie nicht, ob die Signatur fehlte oder falsch war.

Das Beispiel unten zeigt den ganzen Handler: Signaturprüfung, ein kleines Body-Limit, Validierung der Felder, eine Aktualisierung, die nur eine noch nicht bezahlte Bestellung ändert, und eine Antwort mit genau 200. Bewahren Sie den Schlüssel in einer Umgebungsvariable oder einem Secrets-Manager auf, nie im Repository oder in Frontend-Code.

Node.jswebhook.js
import express from 'express';
import { Qvard } from '@qvard/node';

const secret = process.env.QVARD_SECRET_KEY;
const qvard = new Qvard(secret);
const app = express();

app.post('/webhooks/qvard', express.raw({ type: 'application/json', limit: '10kb' }), async (req, res) => {
  let event;
  try {
    event = qvard.webhooks.constructEvent(req.body, req.get('X-Qvard-Signature'), secret);
  } catch {
    return res.sendStatus(400);
  }

  const { id, status } = event;
  if (typeof id !== 'string' || status !== 'completed') return res.sendStatus(200);

  // Idempotent: the update only matches an order that is not paid yet.
  const changed = await db.orders.markPaidIfNot(id);
  if (changed) queue.push({ type: 'fulfil', orderId: id }); // slow work happens after the answer

  return res.sendStatus(200); // exactly 200
});

Machen Sie den Handler idempotent

Qvard wertet eine Zustellung nur dann als erfolgreich, wenn Ihr Endpunkt genau mit HTTP 200 antwortet. Jeder andere Status, ein Timeout oder eine Weiterleitung zählt als Fehler und wird wiederholt: insgesamt fünf Versuche in rund sechs Minuten. Folglich sieht Ihr Handler dasselbe Ereignis manchmal mehrfach, etwa wenn Ihr Server die Arbeit erledigt hat, die Antwort aber verloren ging.

Entwerfen Sie darauf hin:

  • Machen Sie die Statusänderung bedingt: „diese Bestellung als bezahlt markieren, wenn sie noch nicht bezahlt ist“, idealerweise in einer einzigen Datenbankanweisung, damit zwei parallele Zustellungen nicht beide gewinnen.
  • Starten Sie die Erfüllung (E-Mails, Schlüssel, Zugang) nur, wenn die Aktualisierung tatsächlich eine Zeile geändert hat.
  • Ignorieren Sie Ereignisse zu unbekannten Bestellungen, antworten Sie aber trotzdem mit 200; sonst wiederholt Qvard etwas, das Sie nie bearbeiten werden.
  • Handeln Sie nur bei completed. Kein anderer Status darf eine Bestellung als bezahlt markieren.

Das ist zugleich die Abwehr gegen eine erneut abgespielte Anfrage: Ein wiederholtes gültiges Ereignis ändert beim zweiten Mal nichts.

Schnell antworten, später arbeiten

Ihr Endpunkt muss antworten, bevor Qvard aufgibt; halten Sie ihn also kurz: prüfen, aktualisieren, in die Queue stellen, antworten. Die langsamen Teile, etwa E-Mails senden, andere Systeme aufrufen und Dateien erzeugen, erledigen Sie nach der Antwort aus einer Queue.

Denken Sie auch daran, was Ihr Endpunkt nicht weiß. Der Body trägt nur id und status, der erwartete Betrag stammt also aus Ihrer eigenen Bestellung, nicht aus dem Webhook. Übernehmen Sie nie einen Betrag, eine Währung oder ein Produkt aus dem Request-Body.

Bieten Sie den Endpunkt nur über HTTPS an und stellen Sie sicher, dass kein Proxy und keine Weiterleitung davorsteht: Weiterleitungen werden nicht verfolgt, ein 301 von http auf https ist also eine fehlgeschlagene Zustellung.

Logs, Überwachung und Wiederherstellung

Behandeln Sie Webhooks wie Geldbewegungen und führen Sie eine Spur.

  • Protokollieren Sie Ihre Entscheidung, nicht das Geheimnis. Speichern Sie id, status, Zeit und Ihre Entscheidung (angewendet, Duplikat, unbekannte Bestellung, Schlüssel abgelehnt). Protokollieren Sie nie Ihren geheimen Schlüssel.
  • Alarmieren Sie bei Ablehnungen. Eine plötzliche Serie von 400-Antworten bedeutet, dass jemand den Endpunkt abtastet oder Sie Ihren Schlüssel an einer Stelle gewechselt haben und nicht überall.
  • Gleichen Sie ab. Vergleichen Sie einmal täglich die Bestellungen, die Sie als bezahlt ansehen, mit Ihrem Guthaben und den Zahlungen im Qvard-Konto.
  • Stellen Sie wieder her. War Ihr Server länger ausgefallen als das Wiederholungsfenster, senden Sie die Benachrichtigung für diese Zahlung aus dem Konto erneut und lassen Sie Ihren idempotenten Handler damit umgehen.

Wenn Sie vermuten, dass der Schlüssel geleakt ist, ersetzen Sie ihn und prüfen Sie, welche Ihrer Systeme ihn verwendet haben.

Checkliste vor dem Livegang

  • Der Endpunkt nutzt HTTPS, ist öffentlich und von außerhalb Ihres Netzwerks erreichbar.
  • Er prüft den Header X-Qvard-Signature über den rohen Body und antwortet sonst mit 400.
  • Er begrenzt die Body-Größe und validiert id und status.
  • Er markiert eine Bestellung nur bei completed als bezahlt und nur, wenn sie es noch nicht ist.
  • Er antwortet schnell mit genau 200 und erledigt langsame Arbeit danach.
  • Er vertraut nie auf Beträge oder Produkte aus dem Body.
  • Er protokolliert nie den Schlüssel.
  • Sie haben eine fehlschlagende Antwort, eine doppelte Zustellung und eine unbekannte Bestellung getestet.

Den vollständigen Ablauf rund um den Webhook finden Sie unter Krypto-Zahlungen auf einer Website akzeptieren. Die Anfrage- und Antwortformate stehen in der Entwicklerdokumentation.

FAQ

Signiert Qvard Webhooks mit HMAC?

Ja. Jeder Webhook trägt den Header X-Qvard-Signature (t=<unix>,v1=<hex>): ein HMAC-SHA256 aus Zeitstempel und rohem Body, berechnet mit Ihrem geheimen Schlüssel. Prüfen Sie ihn über den rohen Body in konstanter Zeit und behandeln Sie Ihren Schlüssel als Geheimnis.

Welchen Statuscode muss mein Endpunkt zurückgeben?

Genau 200. Jeder andere Status, ein Timeout oder eine Weiterleitung zählt als fehlgeschlagene Zustellung und wird wiederholt, insgesamt fünf Versuche in rund sechs Minuten.

Was, wenn mein Server beim Abschluss der Zahlung ausgefallen war?

Die Wiederholungen decken rund sechs Minuten ab. Danach senden Sie die Benachrichtigung für die Zahlung aus dem Konto erneut. Ein idempotenter Handler macht das sicher.

Kann ich dem Betrag im Webhook vertrauen?

Der Webhook trägt nur die Zahlungs-ID und ihren Status. Verwenden Sie den Betrag aus Ihrer eigenen Bestellung und markieren Sie sie nur beim Status completed als bezahlt.

Ähnliche Artikel

Ähnliche Artikel