
Preuve de concept et laboratoire pour une faille de confirmation de payout Hyperswitch : un client_secret factice accompagné d'un montant dans POST /payouts/{id}/confirm augmente le montant de payout stocké.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · hyperswitch-payout-confirm-secret
Hyperswitch 2026.09.21.0 - Juspay
payouts_confirm authentifie pk_ avec check_value_present("client_secret"). Présent. Pas égal. Le confirm des paiements lie toujours : authenticate_client_secret renvoie IR_09 en cas de non-concordance. Le corps du confirm est un PayoutCreateRequest complet. update_payouts_and_payout_attempt écrit amount avant le routage vers le connecteur.
Un client_secret factice plus amount=99900 sur le confirm des payouts stocke 99900. Le confirm des paiements avec le même factice reste IR_09.
| ID | pas encore de CVE |
| CWE | CWE-863, CWE-345 |
| 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 confirm des payouts |
| Auth | pk_ publiable plus l'id du payout ; secret factice |
| Licence | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1 uniquement |
Il faut la clé publiable (publique, checkout) et l'id du payout (lien de payout, tableau de bord marchand, réponse de création côté client). POST /payouts/{id}/confirm avec un secret factice et un nouveau amount. La récupération montre le montant augmenté même lorsque le routage vers le connecteur renvoie 400 IR_39 (aucun connecteur de payout éligible). L'écriture a déjà eu lieu. L'exécution sur une boutique qui possède un processeur de payout est l'étape suivante.
Un invité sans pk_ n'est pas ce bug. Le confirm avec le secret sk_ est côté marchand. Pas un shell. Pas un payout réussi via connecteur sur cette image de lab.
Même produit que le webhook Worldpayxml non signé. Bug différent.
J'ai lu check_value_present, puis les paiements req_cs != pi_cs, puis update_payouts_and_payout_attempt avant payouts_core. L'auth pour pk_ est « checkout SDK ». Le secret est censé lier la session du navigateur à ce payout. Les paiements font une égalité. Les payouts font une présence. Un factice non vide suffit. Le JSON est toujours une requête de création, donc amount n'est pas un no-op lors du confirm.
Le premier client qui regarde ça enverra le confirm sans client_secret et obtiendra MissingRequiredField. Présent. Envoyez le vrai secret et vous êtes le payeur. Envoyez un factice sur le confirm des paiements et vous obtenez IR_09. C'est le contrôle. Envoyez le factice sur le confirm des payouts.
Fausses pistes déjà consignées : traiter le HTTP 400 IR_39 comme « rien ne s'est passé » (récupérez le payout) ; le montant reste à 1000 parce que le confirm a omis amount (unwrap_or conserve l'ancienne valeur) ; payout déjà terminal (Success / Failed / Cancelled) ; un reverse shell. Du théâtre. Le témoin est la récupération amount=99900 après le secret factice, avec le confirm des paiements toujours IR_09.
Ensuite : créez un marchand, une clé secrète, un pk_ publiable, un connecteur payout worldpayxml, et POST /payouts/create avec confirm=false amount 1000. RequiresConfirmation. Contrôle avec secret factice sur les paiements. Factice 99900 sur les payouts. Récupération.
cd lab
./run.sh
Cible uniquement http://127.0.0.1:18083. Ne partagez pas de projet compose avec le lab du webhook.
IOC amount_before=1000 status_before=requires_confirmation
IOC payments.confirm dummy-secret status=400 IR_09 client_secret mismatch
IOC payouts.confirm dummy-secret status=400 IR_39 no eligible connector
IOC retrieve amount=99900 status=requires_confirmation
SUCCESS Hyperswitch payout confirm unbound client_secret amount raise
Comparez client_secret à payouts.client_secret comme le font les paiements, et n'appliquez pas PayoutCreateRequest.amount lors du confirm tant que cette vérification n'a pas réussi. Un secret factice doit donner IR_09 (ou 401), et le montant récupéré doit rester à 1000.
payouts.rs · authentication.rs · payouts/helpers.rs · core/payouts.rs · payments/helpers.rsGNU Affero GPL v3.0. Voir LICENSE. Lab en boucle locale uniquement. Aucune garantie.