Preuve de concept pour une faille de webhook Hyperswitch Worldpayxml non authentifiée (CWE-345/306) qui falsifie du XML SETTLED pour marquer des paiements comme réussis sans capture PSP.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · hyperswitch-unsigned-webhook
Hyperswitch 2026.09.21.0 - Juspay
Worldpayxml::verify_webhook_source renvoie Ok(true) avec un commentaire indiquant que la vérification est effectuée via mTLS. Il n'y a pas de mTLS sur cette route. La fonction ignore le corps et les en-têtes. incoming.rs fait ensuite confiance à source_verified et prend HandleResponse sans PSync. LastEvent::Settled correspond à PaymentIntentSuccess. L'algorithme par défaut de IncomingWebhook est NoAlgorithm. BitPay et Shift4 en héritent. Worldpayxml n'atteint même pas ce stade. Il court-circuite.
Un XML SETTLED factice non authentifié marque le paiement comme réussi. Aucune capture Worldpay. Aucun certificat client.
| ID | pas encore de CVE |
| CWE | CWE-345, CWE-306 |
| CVSS | Élevé : 7.5 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N |
| Produit | Hyperswitch |
| Affecté | jusqu'à 2026.09.21.0 webhooks entrants Worldpayxml |
| Auth | non authentifié ; nécessite merchant_id et orderCode |
| Licence | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1 uniquement |
Il faut merchant_id (présent dans chaque URL du tableau de bord Hyperswitch) et orderCode égal au connector_transaction_id de la tentative. POST /webhooks/{merchant_id}/worldpayxml avec un SETTLED non signé. Si source_verified était faux, incoming déclencherait Trigger et ferait un PSync du connecteur. Worldpayxml ne prend jamais ce chemin. L'intent passe à Charged. Lab : status=succeeded. XML factice. Aucun PSP.
Deviner les identifiants de paiement n'est pas le bug. Forger un SETTLED pour un identifiant déjà observé l'est. Pas un shell. Pas un règlement Worldpay réel.
Même produit, bug différent : payout confirm unbound client_secret.
J'ai lu Ok(true), puis HandleResponse vs Trigger, puis Settled -> PaymentIntentSuccess. Le commentaire mentionne mTLS. Le POST du lab n'a pas de certificat client.
Le premier client qui examine cela va POSTer le webhook contre requires_confirmation. Incoming ne passera pas un paiement à Charged s'il n'a pas quitté la confirmation. Marquez la tentative, ou attendez requires_capture / processing. Le lab effectue le marquage. Ce marquage est un artifice de lab. Une tentative réelle possède déjà l'identifiant du connecteur. Le webhook, non.
Fausses pistes déjà consignées : HMAC signé sur un connecteur qui vérifie réellement (ce chemin ne le fait pas) ; traiter le HTTP 200 du webhook comme l'oracle (interroger GET /payments/{id} jusqu'à status=succeeded) ; un orderCode qui n'est pas connector_transaction_id ; un reverse shell. Du théâtre.
Ensuite : marchand, clé API, MCA worldpayxml, paiement avec confirm=false, marquage de connector_transaction_id, SETTLED non signé, récupération.
cd lab
./run.sh
Cible uniquement http://127.0.0.1:18082. run.sh clone le tag 2026.09.21.0 dans lab/hyperswitch-src pour la configuration et les migrations.
IOC status_before=requires_confirmation
IOC webhook status=200 unsigned lastEvent=SETTLED
IOC poll status=succeeded
SUCCESS Hyperswitch unsigned Worldpayxml webhook Charged
Vérifier la notification Worldpay (signature / mTLS que la route termine réellement). Ne pas faire de HandleResponse sur un connecteur qui a renvoyé Ok(true) depuis une opération vide. Un SETTLED non signé ne doit pas faire passer l'intent à succeeded.
worldpayxml.rs · transformers.rs · incoming.rs · webhooks.rs · crypto.rsGNU Affero GPL v3.0. Voir LICENSE. Lab en boucle locale uniquement. Aucune garantie.