Как ДОЛЖНА работать Web3 аутентификация
Нормальный flow аутентификации через кошелёк:
1. Клиент серверу: "Хочу войти, мой адрес 0xАБВ"
2. Сервер клиенту: "Подпиши этот уникальный nonce: a7f3b2..."
3. Клиент подписывает nonce приватным ключом в MetaMask
4. Клиент серверу: "Вот подпись: 0x8e3f..."
5. Сервер проверяет подпись через ecrecover - совпадает ли она с адресом 0xАБВ
6. Только после проверки - выдаёт JWT
Смысл в том, что приватный ключ есть только у владельца кошелька. Без подписи доказать владение адресом невозможно.
Как это работает у исследуемого проекта
1. Клиент серверу: "Мой адрес 0xАБВ"
2. Сервер: "Ок, держи JWT" - всё, конец
Шаги 2-5 просто отсутствуют. Сервер верит на слово.
Более того, endpoint
/api/auth/verify который по названию должен проверять подпись - принимает параметры signature и nonce, но полностью их игнорирует:# Фейковая подпись "FAKE" - принимается
curl -sk -X POST 'https://example.com/api/auth/verify' \
-H 'Content-Type: application/json' \
-d '{"wallet_address":"0x.....","signature":"FAKE","nonce":"FAKE"}'
# Ответ: 200 OK + валидный JWT
Почему это критично
Ethereum адрес - публичная информация. Любой адрес можно посмотреть на etherscan.io. Это как логин без пароля. Зная адрес кошелька пользователя, атакующий:
1. Отправляет один POST запрос с этим адресом
2. Получает JWT токен
3. С этим токеном читает
/api/user - а там в ответе прилетают password_hash, two_fa_code, reset_token4. Полный захват аккаунта
Почему это работает (вероятная причина)
Скорее всего, разработчики:
- Написали auth endpoints как заглушки с планом "потом добавим проверку подписи"
- Использовали catch-all route (
/api/auth/[...slug]) в Next.js, который на любой POST с wallet_address создаёт/находит пользователя и выдаёт JWT- Функция
ecrecover или verifyMessage либо не вызывается, либо её результат не проверяется перед выдачей токена- Все 8 auth endpoints проходят через один и тот же handler, который сразу делает
jwt.sign({userId: user.id}) без проверкиВ клиентском JS видно, что фронтенд всё делает правильно - вызывает MetaMask, получает подпись, отправляет её. Но бэкенд эту подпись не проверяет.
С учетом того что у проекта есть свой токен - становится не сложным поиск адресов всех холдеров/пользователей этого проекта.
