PoC для неаутентифицированной уязвимости вебхука Hyperswitch Worldpayxml (CWE-345/306), которая подделывает XML-статус SETTLED, чтобы помечать платежи успешными без захвата в PSP.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · hyperswitch-unsigned-webhook
Hyperswitch 2026.09.21.0 - Juspay
Worldpayxml::verify_webhook_source возвращает Ok(true) с комментарием, что проверка выполняется через mTLS. На этом маршруте mTLS нет. Функция игнорирует тело и заголовки. Затем incoming.rs доверяет source_verified и выполняет HandleResponse без PSync. LastEvent::Settled отображается на PaymentIntentSuccess. Алгоритм IncomingWebhook по умолчанию — NoAlgorithm. BitPay и Shift4 наследуют это. Worldpayxml даже до этого не доходит. Он завершается досрочно.
Неаутентифицированный фиктивный SETTLED XML помечает платёж как успешный. Без захвата Worldpay. Без клиентского сертификата.
| ID | пока нет 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 |
| Продукт | Hyperswitch |
| Затронуто | вплоть до 2026.09.21.0 входящие вебхуки Worldpayxml |
| Аутентификация | без аутентификации; нужны merchant_id и orderCode |
| Лицензия | GNU Affero GPL v3.0 |
| Лаборатория | только 127.0.0.1 |
Нужны merchant_id (есть в каждом URL панели Hyperswitch) и orderCode, равный connector_transaction_id попытки. POST /webhooks/{merchant_id}/worldpayxml с неподписанным SETTLED. Если бы source_verified был false, incoming выполнил бы Trigger и PSync коннектора. Worldpayxml никогда не идёт по этому пути. Intent переходит в Charged. В лаборатории: status=succeeded. Фиктивный XML. Без PSP.
Угадывание идентификаторов платежей — не баг. Подделка SETTLED для идентификатора, который вы уже видели, — баг. Это не шелл. И не реальный расчёт через Worldpay.
Тот же продукт, другой баг: payout confirm unbound client_secret.
Я прочитал Ok(true), затем HandleResponse против Trigger, затем Settled -> PaymentIntentSuccess. В комментарии сказано mTLS. В лабораторном POST нет клиентского сертификата.
Первый клиент, который на это посмотрит, отправит вебхук против requires_confirmation. Incoming не переведёт в Charged платёж, который ещё не покинул подтверждение. Проставьте метку на попытку или дождитесь requires_capture / processing. В лаборатории метка ставится. Эта метка — лабораторная фикстура. У живой попытки уже есть идентификатор коннектора. У вебхука — нет.
Уже зафиксированные ошибочные пути: подписанный HMAC на коннекторе, который действительно проверяет (этот путь — нет); трактовка HTTP 200 вебхука как оракула (опрашивайте GET /payments/{id} до status=succeeded); orderCode, который не является connector_transaction_id; обратный шелл. Театр.
Затем: merchant, API-ключ, MCA worldpayxml, платёж с confirm=false, метка connector_transaction_id, неподписанный SETTLED, retrieve.
cd lab
./run.sh
Цель только http://127.0.0.1:18082. run.sh клонирует тег 2026.09.21.0 в lab/hyperswitch-src для конфигурации и миграций.
IOC status_before=requires_confirmation
IOC webhook status=200 unsigned lastEvent=SETTLED
IOC poll status=succeeded
SUCCESS Hyperswitch unsigned Worldpayxml webhook Charged
Проверяйте уведомление Worldpay (подпись / mTLS, который маршрут действительно терминирует). Не выполняйте HandleResponse на коннекторе, вернувшем Ok(true) из no-op. Неподписанный SETTLED не должен переводить intent в succeeded.
worldpayxml.rs · transformers.rs · incoming.rs · webhooks.rs · crypto.rsGNU Affero GPL v3.0. См. LICENSE. Только лаборатория на loopback. Без гарантий.