
Proof-of-Concept für eine nicht authentifizierte Hyperswitch-Worldpayxml-Webhook-Schwachstelle (CWE-345/306), die SETTLED-XML fälscht, um Zahlungen als erfolgreich zu markieren, ohne PSP-Capture.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · hyperswitch-unsigned-webhook
Hyperswitch 2026.09.21.0 - Juspay
Worldpayxml::verify_webhook_source gibt Ok(true) zurück mit einem Kommentar, dass die Verifikation über mTLS erfolgt. Auf dieser Route gibt es kein mTLS. Die Funktion ignoriert den Body und die Header. incoming.rs vertraut dann auf source_verified und nimmt HandleResponse ohne PSync. LastEvent::Settled wird auf PaymentIntentSuccess abgebildet. Der Standard-IncomingWebhook-Algorithmus ist NoAlgorithm. BitPay und Shift4 erben das. Worldpayxml kommt nicht einmal so weit. Es schließt kurz.
Unauthentifiziertes Dummy-SETTLED-XML markiert die Zahlung als erfolgreich. Kein Worldpay-Capture. Kein Client-Zertifikat.
| ID | noch keine CVE |
| CWE | CWE-345, CWE-306 |
| CVSS | High: 7.5 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N |
| Produkt | Hyperswitch |
| Betroffen | bis 2026.09.21.0 Worldpayxml eingehende Webhooks |
| Auth | unauthentifiziert; benötigt merchant_id und orderCode |
| Lizenz | GNU Affero GPL v3.0 |
| Lab | nur 127.0.0.1 |
Benötigt merchant_id (in jeder Hyperswitch-Dashboard-URL) und orderCode gleich der connector_transaction_id des Versuchs. POST /webhooks/{merchant_id}/worldpayxml unsigniertes SETTLED. Wäre source_verified false, würde incoming Trigger ausführen und den Connector PSyncen. Worldpayxml nimmt diesen Pfad nie. Intent geht auf Charged. Lab: status=succeeded. Dummy-XML. Kein PSP.
Das Erraten von Payment-IDs ist nicht der Bug. Das Fälschen von SETTLED für eine ID, die man bereits gesehen hat, ist es. Keine Shell. Keine echte Worldpay-Settlement.
Gleiches Produkt, anderer Bug: payout confirm unbound client_secret.
Ich las Ok(true), dann HandleResponse vs Trigger, dann Settled -> PaymentIntentSuccess. Der Kommentar sagt mTLS. Der Lab-POST hat kein Client-Zertifikat.
Der erste Client, der sich das ansieht, wird den Webhook gegen requires_confirmation POSTen. Incoming wird eine Zahlung, die die Bestätigung noch nicht verlassen hat, nicht auf Charged setzen. Den Versuch stempeln oder warten, bis requires_capture / processing. Das Lab macht das Stempeln. Dieser Stempel ist ein Lab-Fixture. Ein Live-Versuch hat bereits die Connector-ID. Der Webhook nicht.
Bereits dokumentierte Irrwege: signiertes HMAC auf einem Connector, der tatsächlich verifiziert (dieser Pfad tut das nicht); Webhook-HTTP-200 als Orakel behandeln (GET /payments/{id} pollen, bis status=succeeded); orderCode, das nicht connector_transaction_id ist; eine Reverse Shell. Theater.
Dann: Merchant, API-Key, worldpayxml MCA, Zahlung mit confirm=false, connector_transaction_id stempeln, unsigniertes SETTLED, abrufen.
cd lab
./run.sh
Ziel nur http://127.0.0.1:18082. run.sh klont Tag 2026.09.21.0 nach lab/hyperswitch-src für Konfiguration und Migrationen.
IOC status_before=requires_confirmation
IOC webhook status=200 unsigned lastEvent=SETTLED
IOC poll status=succeeded
SUCCESS Hyperswitch unsigned Worldpayxml webhook Charged
Die Worldpay-Notify verifizieren (Signatur / mTLS, die die Route tatsächlich terminiert). Kein HandleResponse bei einem Connector, der Ok(true) von einem No-Op zurückgegeben hat. Unsigniertes SETTLED darf den Intent nicht auf succeeded setzen.
worldpayxml.rs · transformers.rs · incoming.rs · webhooks.rs · crypto.rsGNU Affero GPL v3.0. Siehe LICENSE. Nur Loopback-Lab. Keine Gewährleistung.