Злоумышленники опубликовали вредоносную версию популярного npm-пакета @injectivelabs/sdk-ts (около 50 тысяч загрузок в неделю), предназначенного для работы с блокчейном Injective. Версия 1.20.21 содержала код, который перехватывал сид-фразы (мнемонические фразы) и приватные ключи, генерируемые при нормальной работе приложения, и отправлял их на сервер, находящийся под контролем атакующих.
Описание
Вредоносная версия находилась в реестре npm менее часа 8 июня 2026 года, после чего разработчик пакета обнаружил проблему и опубликовал исправление. Однако за это время атакующий успел перевыпустить ту же версию 1.20.21 для 17 других пакетов в пространстве имён @injectivelabs, каждый из которых имел жёсткую зависимость от отравленного SDK. Таким образом, любой проект, использующий любой из этих зависимых пакетов, транзитивно получал вредоносный код, даже если сам не указывал sdk-ts в своём манифесте.
Полезная нагрузка была небольшой и выглядела как обычная инструментальная вставка для сбора анонимной статистики. Она не выполнялась во время установки пакета и не нарушала штатное поведение SDK. Код активировался только тогда, когда приложение в ходе нормального использования генерировало ключ, после чего тихо копировал секретную информацию и отправлял её наружу.
Согласно данным Aikido, поставщика решений для безопасности цепочек поставок ПО, вредоносный код располагался в двух сборочных артефактах: dist/cjs/accounts-Cy0p4lLW.cjs и dist/esm/accounts-jQ1GSgaW.js. Сравнение с чистой версией 1.20.23 показало, что файлы были идентичны за исключением одного вставленного блока и двух однострочных хуков. Блок был оформлен как src/utils/key-derivation-telemetry.ts (телесбор данных о генерации ключей) с комментарием об анонимном сборе метрик для оптимизации SDK. Никакой анонимности на самом деле не было.
Два хука располагались внутри методов PrivateKey.fromMnemonic и PrivateKey.fromHex - стандартных точек входа, через которые приложение превращает сид-фразу или шестнадцатеричный ключ в используемый кошелёк. Каждый хук выполнялся до начала реальной генерации ключа, так что секрет захватывался при каждом легитимном вызове. Для мнемонической фразы использовался маркер "fm", для приватного ключа - "fh". Это позволяло атакующему определить тип похищенного секрета и восстановить кошелёк на своей стороне.
Функция trackKeyDerivation помещала запись с меткой времени в очередь в памяти и запускала процедуру отправки. Хост для приёма данных был закодирован в виде массива кодов символов, которые собирались в строку только во время выполнения. Такой приём затруднял поиск домена простым текстовым поиском в сборке. Декодированный адрес - testnet[.]archival[.]chain[.]grpc-web[.]injective[.]network - был оформлен как поддомен инфраструктуры gRPC-web самого Injective, поэтому трафик с похищенными данными смешивался с обычными сетевыми вызовами легитимного приложения.
Отправка происходила с задержкой в 2 секунды: записи из очереди объединялись символом |, затем весь блок кодировался в Base64 и помещался в HTTP-заголовок X-Request-Id. Тело запроса было пустым, а тип содержимого указывался как application/grpc-web+proto. Таким образом, на сетевом уровне запрос выглядел как обычный gRPC-вызов с идентификатором запроса. Любые ошибки при отправке молча перехватывались пустым catch, поэтому пользователь никогда не получал уведомление о неудачной эксфильтрации.
Поскольку триггером была генерация ключа во время выполнения, а не скрипт установки, сканеры и песочницы, проверяющие только фазу postinstall, не обнаруживали ничего подозрительного. В манифесте пакета не было установочных хуков.
Из 17 перевыпущенных пакетов ни один не содержал изменений в собственном исходном коде. Атакующий лишь переопубликовал их с жёсткой привязкой версии к 1.20.21 отравленного SDK. Например, проект, установивший @injectivelabs/wallet-core@1.20.21, разрешал зависимость до вредоносного SDK транзитивно, и код кражи запускался, даже если разработчик не указывал sdk-ts напрямую. Названия кошелёчных пакетов точно соответствовали тем сценариям, на сбор секретов из которых была нацелена нагрузка.
Компрометация произошла через коммиты с GitHub-аккаунта, имевшего длительную историю в репозитории. Это указывает на захват учётной записи, а не на случайную атаку. Вредоносная версия 1.20.21 была опубликована 8 июня 2026 года в 22:59 по центральноевропейскому летнему времени, разработчик откатил изменения в 23:18, а чистая версия вышла в 23:48. По статистике npm вредоносную версию скачали 310 раз - небольшое число на фоне 50 тысяч еженедельных загрузок и 87 зависимых пакетов.
Тем не менее, устранение угрозы не было завершено. На момент публикации отчёта вредоносная версия была помечена на npm как устаревшая с предупреждением, но не удалена, поэтому её по-прежнему можно скачать. Сборочные артефакты скомпрометированной версии оставались на странице релизов проекта на GitHub. Любая мнемоническая фраза или приватный ключ, прошедшие через эти пакеты, должны считаться скомпрометированными. Рекомендуется переместить средства на новые кошельки, а затем сменить ключи и сид-фразы.
Этот инцидент демонстрирует, что атаки на цепочки поставок ПО становятся всё более изощрёнными. Вредоносный код маскируется под легитимную телеметрию, использует кодирование хоста и подделывается под нормальный сетевой трафик. Единственным барьером в данном случае стала быстрая реакция разработчика, ограничившая окно уязвимости менее чем часом. Для защиты от подобных угроз разработчикам рекомендуется проверять не только прямые, но и транзитивные зависимости, а также использовать инструменты динамического анализа, которые отслеживают поведение кода во время выполнения, а не только на этапе установки.
Индикаторы компрометации
URL
- https://testnet.archival.chain.grpc-web.injective.network/
SHA256
- 103c4e6181151c1bcfedc41506cd1815458c38375d08a8fcd9981dbe0b965ce0
- 9a59eb454f3ca3fe91214136ee5edd417cc47a80e6f169b52099d6561944baf9
Affected package
- @injectivelabs/sdk-ts@1.20.21 (contains the stealer)
- @injectivelabs/utils@1.20.21
- @injectivelabs/networks@1.20.21
- @injectivelabs/ts-types@1.20.21
- @injectivelabs/exceptions@1.20.21
- @injectivelabs/wallet-base@1.20.21
- @injectivelabs/wallet-core@1.20.21
- @injectivelabs/wallet-cosmos@1.20.21
- @injectivelabs/wallet-private-key@1.20.21
- @injectivelabs/wallet-evm@1.20.21
- @injectivelabs/wallet-trezor@1.20.21
- @injectivelabs/wallet-cosmostation@1.20.21
- @injectivelabs/wallet-ledger@1.20.21
- @injectivelabs/wallet-wallet-connect@1.20.21
- @injectivelabs/wallet-magic@1.20.21
- @injectivelabs/wallet-strategy@1.20.21
- @injectivelabs/wallet-turnkey@1.20.21
- @injectivelabs/wallet-cosmos-strategy@1.20.21