
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
Write-up: https://abraxaslabs.tech/research/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 erfolge. 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-Algorithmus von IncomingWebhook ist NoAlgorithm. BitPay und Shift4 erben das. Worldpayxml kommt nicht einmal so weit. Es bricht vorzeitig ab.
Unauthentifiziertes Dummy-SETTLED-XML markiert die Zahlung als erfolgreich. Keine 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 eingehende Worldpayxml-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 Attempts. POST von unsigniertem SETTLED an /webhooks/{merchant_id}/worldpayxml. Wäre source_verified false, würde incoming Trigger ausführen und den Connector per PSync abfragen. Worldpayxml nimmt diesen Pfad nie. Der Intent wird 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 Attempt stempeln oder warten, bis requires_capture / processing erreicht ist. Das Lab macht das Stempeln. Dieser Stempel ist ein Lab-Fixture. Ein Live-Attempt hat bereits die Connector-ID. Der Webhook nicht.
Bereits dokumentierte Irrwege: signiertes HMAC auf einem Connector, der tatsächlich verifiziert (dieser Pfad tut es nicht); das Behandeln von Webhook-HTTP 200 als Orakel (stattdessen GET /payments/{id} pollen, bis status=succeeded); orderCode, das nicht connector_transaction_id ist; eine Reverse Shell. Theater.
Dann: Merchant, API-Key, worldpayxml-MCA, Payment mit confirm=false, connector_transaction_id stempeln, unsigniertes SETTLED, abrufen.
cd lab
./run.sh
Ziel ist ausschließlich http://127.0.0.1:18082. run.sh klont den 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
Verifiziere die Worldpay-Benachrichtigung (Signatur / mTLS, die die Route tatsächlich terminiert). Rufe kein HandleResponse auf einem Connector auf, der Ok(true) von einem No-op zurückgegeben hat. Ein unsigniertes SETTLED darf die 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.