Как 5.6 МБ JavaScript раскрыли скрытый бэкенд и подпись произвольных платежей
Аудит Web3-проекта с крипто-пресейлом. Фронтенд - React SPA за Cloudflare. Ни одного видимого API-эндпоинта. Казалось бы - нечего ломать.
React SPA отдаёт один index.html на любой путь. WordPress-поддомены закрыты Cloudflare. REST API отключён. Регистрация отключена. Живых эндпоинтов - ноль.
Но у Web3-проектов есть особенность: вся логика взаимодействия с блокчейном живёт на клиенте. Кошелёк подключается из браузера, адреса контрактов и RPC-вызовы формируются на фронте. Сервер не может это спрятать. Когда видишь пустую SPA-оболочку без единого API - это не тупик. Это приглашение читать бандлы.
Vite нарезал код на 10 чанков. Source maps недоступны (все .js.map возвращают HTML фронтенда - SPA catch-all). Только ручной разбор минифицированного кода.
app-[hash].js 434 KB - бизнес-логика, роуты, платежи
main-[hash].js 576 KB - WalletConnect, UI
wallet-libs-[hash] 3.5 MB - кошельки, провайдеры, цепочки
crypto-libs-[hash] 637 KB - криптобиблиотеки
Ищу строки: https://, baseURL, fetch(, axios, endpoint. В чанке бизнес-логики нахожу базовый URL, который нигде не фигурировал - ни в DNS, ни в документации, ни в HTML. Бэкенд жил на полностью отдельном домене.
Один GET на /health подтвердил:
{"status":"ok","info":{"mongodb":{"status":"up"},"google":{"status":"up"}}}
CORS настроен правильно (только домен фронтенда), но GET-эндпоинты не требуют аутентификации - прямые curl-запросы работают.
Дальше - по всему JS-бандлу собираю пути, параметры, заголовки. Восстановил 17 эндпоинтов. Каждый проверил вручную - какие требуют авторизацию, какие нет, какие принимают пользовательский ввод и как его валидируют.
Большинство GET-эндпоинтов оказались открыты: финансовая конфигурация пресейла, баланс любого кошелька по адресу (IDOR), статистика стейкинг-пулов, валидация промокодов (перебор). Неприятно, но не критично.
Самый интересный - POST /wert.
Wert - фиатный шлюз. Пользователь платит картой, Wert выполняет on-chain транзакцию. Бэкенд подписывает параметры платежа, и эта подпись говорит Wert: «транзакция легитимна».
Эндпоинт /wert принимает POST с параметрами и возвращает подписанные данные. Проверяю - что если подменить адрес смарт-контракта?
# Отправляю произвольный:
curl -X POST https:///wert \
-H "Content-Type: application/json" \
-d '{"sc_address":"0x0000000000000000000000000000000000000001",
"sc_input_data":"0xdeadbeef",
"commodity_amount":100}'
Ответ: валидная подпись.
Подменяю sc_address на произвольный контракт - подписано. Подменяю sc_input_data на произвольные calldata - подписано. Ставлю commodity_amount = -1 - подписано. Ставлю 0 - тоже подписано.
Сервер подписывает всё. Нет валидации адреса, нет проверки calldata, нет ограничений на сумму. Если Wert доверяет подписи без собственной валидации - атакующий подменяет sc_address на свой контракт, получает подпись, инициирует платёж, и средства пользователя уходят не туда.
Бонус: RPC-ключ с debug-доступом к 5 блокчейнам
В том же бандле - API-ключ RPC-провайдера. Рабочий на 5 сетях (BSC, Ethereum, ETH Sepolia, Solana Mainnet, Solana Devnet) с premium-методами:
# debug_traceBlockByNumber - трассировка ВСЕХ транзакций блока
# (опкоды, стек, память, internal calls)
curl -X POST "https:///$KEY" \
-d '{"method":"debug_traceBlockByNumber","params":["latest",{}]}'
# Получаю 200 OK
# txpool_status - мемпул
# {"pending":"0x2d","queued":"0x2ee"}
Debug + archive - premium-тариф. Вектор для MEV/фронтраннинга + нагрузка на платный план проекта.