Аудит 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://<backend>/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://<rpc>/$KEY" \
-d '{"method":"debug_traceBlockByNumber","params":["latest",{}]}'
# Получаю 200 OK
# txpool_status - мемпул
# {"pending":"0x2d","queued":"0x2ee"}
Debug + archive - premium-тариф. Вектор для MEV/фронтраннинга + нагрузка на платный план проекта.