
Proof-of-concept per una vulnerabilità non autenticata del webhook Hyperswitch Worldpayxml (CWE-345/306) che falsifica XML SETTLED per contrassegnare i pagamenti come riusciti senza la cattura del PSP.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · hyperswitch-unsigned-webhook
Hyperswitch 2026.09.21.0 - Juspay
Worldpayxml::verify_webhook_source restituisce Ok(true) con un commento che afferma che la verifica avviene tramite mTLS. Non c'è mTLS su questa route. La funzione ignora il body e gli header. incoming.rs poi si fida di source_verified ed esegue HandleResponse senza PSync. LastEvent::Settled viene mappato su PaymentIntentSuccess. L'algoritmo predefinito di IncomingWebhook è NoAlgorithm. BitPay e Shift4 ereditano questo comportamento. Worldpayxml non arriva nemmeno a quel punto. Va in cortocircuito.
Un XML SETTLED fittizio non autenticato contrassegna il pagamento come riuscito. Nessuna cattura Worldpay. Nessun certificato client.
| ID | nessuna CVE al momento |
| 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 |
| Prodotto | Hyperswitch |
| Affetto | fino alla 2026.09.21.0 webhook in ingresso Worldpayxml |
| Auth | non autenticato; servono merchant_id e orderCode |
| Licenza | GNU Affero GPL v3.0 |
| Lab | solo 127.0.0.1 |
Servono merchant_id (presente in ogni URL della dashboard Hyperswitch) e orderCode uguale al connector_transaction_id del tentativo. POST /webhooks/{merchant_id}/worldpayxml con SETTLED non firmato. Se source_verified fosse false, incoming eseguirebbe Trigger e PSync sul connector. Worldpayxml non prende mai quella strada. L'intent diventa Charged. Lab: status=succeeded. XML fittizio. Nessun PSP.
Indovinare i payment id non è il bug. Falsificare SETTLED per un id che hai già visto lo è. Non è una shell. Non è un regolamento Worldpay reale.
Stesso prodotto, bug diverso: payout confirm unbound client_secret.
Ho letto Ok(true), poi HandleResponse vs Trigger, poi Settled -> PaymentIntentSuccess. Il commento dice mTLS. Il POST del lab non ha certificato client.
Il primo client che guarda questo farà il POST del webhook contro requires_confirmation. Incoming non porterà a Charged un pagamento che non è uscito dalla conferma. Marca il tentativo, oppure aspetta fino a requires_capture / processing. Il lab fa la marcatura. Quella marcatura è un fixture del lab. Un tentativo reale ha già il connector id. Il webhook no.
Vicoli ciechi già registrati: HMAC firmato su un connector che verifica davvero (questo percorso non lo fa); trattare l'HTTP 200 del webhook come oracolo (fare polling su GET /payments/{id} fino a status=succeeded); orderCode che non è connector_transaction_id; una reverse shell. Teatro.
Poi: merchant, API key, MCA worldpayxml, pagamento con confirm=false, marca connector_transaction_id, SETTLED non firmato, retrieve.
cd lab
./run.sh
Target solo http://127.0.0.1:18082. run.sh clona il tag 2026.09.21.0 in lab/hyperswitch-src per config e migrazioni.
IOC status_before=requires_confirmation
IOC webhook status=200 unsigned lastEvent=SETTLED
IOC poll status=succeeded
SUCCESS Hyperswitch unsigned Worldpayxml webhook Charged
Verificare la notify Worldpay (firma / mTLS che la route termina effettivamente). Non eseguire HandleResponse su un connector che ha restituito Ok(true) da un no-op. Un SETTLED non firmato non deve portare l'intent a succeeded.
worldpayxml.rs · transformers.rs · incoming.rs · webhooks.rs · crypto.rsGNU Affero GPL v3.0. Vedi LICENSE. Solo lab su loopback. Nessuna garanzia.