
Prova de conceito para uma falha de webhook Hyperswitch Worldpayxml não autenticada (CWE-345/306) que forja XML SETTLED para marcar pagamentos como bem-sucedidos sem captura no PSP.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · hyperswitch-unsigned-webhook
Hyperswitch 2026.09.21.0 - Juspay
Worldpayxml::verify_webhook_source retorna Ok(true) com um comentário de que a verificação é feita via mTLS. Não há mTLS nessa rota. A função ignora o corpo e os cabeçalhos. incoming.rs então confia em source_verified e executa HandleResponse sem PSync. LastEvent::Settled mapeia para PaymentIntentSuccess. O algoritmo padrão de IncomingWebhook é NoAlgorithm. BitPay e Shift4 herdam isso. Worldpayxml nem chega tão longe. Ele faz short-circuit.
XML SETTLED fictício não autenticado marca o pagamento como bem-sucedido. Sem captura Worldpay. Sem certificado de cliente.
| ID | sem CVE ainda |
| CWE | CWE-345, CWE-306 |
| CVSS | Alto: 7.5 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N |
| Produto | Hyperswitch |
| Afetado | até 2026.09.21.0 webhooks de entrada Worldpayxml |
| Autenticação | não autenticado; precisa de merchant_id e orderCode |
| Licença | GNU Affero GPL v3.0 |
| Lab | apenas 127.0.0.1 |
Precisa de merchant_id (em toda URL do painel Hyperswitch) e orderCode igual ao connector_transaction_id da tentativa. POST /webhooks/{merchant_id}/worldpayxml com SETTLED não assinado. Se source_verified fosse falso, incoming faria Trigger e PSync no conector. Worldpayxml nunca segue esse caminho. A intenção vai para Charged. Lab: status=succeeded. XML fictício. Sem PSP.
Adivinhar ids de pagamento não é o bug. Forjar SETTLED para um id que você já viu é. Não é um shell. Não é uma liquidação Worldpay real.
Mesmo produto, bug diferente: payout confirm unbound client_secret.
Eu li Ok(true), depois HandleResponse vs Trigger, depois Settled -> PaymentIntentSuccess. O comentário diz mTLS. O POST do lab não tem certificado de cliente.
O primeiro cliente que olhar para isso vai fazer POST do webhook contra requires_confirmation. Incoming não vai marcar como Charged um pagamento que ainda não saiu da confirmação. Carimbe a tentativa, ou espere até requires_capture / processing. O lab faz o carimbo. Esse carimbo é um artefato do lab. Uma tentativa real já tem o id do conector. O webhook não.
Caminhos errados já registrados: HMAC assinado em um conector que realmente verifica (esse caminho não verifica); tratar o HTTP 200 do webhook como o oráculo (faça polling em GET /payments/{id} até status=succeeded); orderCode que não é connector_transaction_id; um reverse shell. Teatro.
Então: merchant, API key, MCA worldpayxml, pagamento com confirm=false, carimbar connector_transaction_id, SETTLED não assinado, retrieve.
cd lab
./run.sh
Alvo apenas http://127.0.0.1:18082. run.sh clona a tag 2026.09.21.0 em lab/hyperswitch-src para config e migrations.
IOC status_before=requires_confirmation
IOC webhook status=200 unsigned lastEvent=SETTLED
IOC poll status=succeeded
SUCCESS Hyperswitch unsigned Worldpayxml webhook Charged
Verifique o notify do Worldpay (assinatura / mTLS que a rota realmente termina). Não faça HandleResponse em um conector que retornou Ok(true) de um no-op. SETTLED não assinado não deve mover a intenção para succeeded.
worldpayxml.rs · transformers.rs · incoming.rs · webhooks.rs · crypto.rsGNU Affero GPL v3.0. Veja LICENSE. Apenas lab em loopback. Sem garantia.