Популярный npm-пакет ai-sdk-ollama, который служит неофициальным мостом между инструментом локального запуска больших языковых моделей Ollama и фреймворком Vercel AI SDK, стал целью сложной атаки на цепочку поставок. Исследователи из компании Endor Labs зафиксировали публикацию сразу четырёх вредоносных версий этого пакета в течение 17 секунд. Инцидент затронул версии 0.13.1, 1.1.1, 2.2.1 и 3.8.5 - последнюю из них реестр npm пометил тегом latest, что делает её автоматически рекомендуемой при установке без жёсткой фиксации версии.
Описание
Пакет ai-sdk-ollama не принадлежит ни разработчикам Ollama, ни создателям Vercel AI SDK. Однако его популярность высока: ежемесячно он загружается более 120 тысяч раз. Именно широкая аудитория сделала его привлекательной мишенью. Злоумышленники не трогали оригинальный код библиотеки. Вместо этого они добавили в публикуемый архив два новых файла и использовали особенность поведения npm при установке для запуска вредоносной полезной нагрузки ещё до того, как какой-либо код приложения импортирует сам пакет.
Ключевым приёмом стал файл binding.gyp, а не традиционный сценарий postinstall. Дело в том, что npm автоматически запускает команду node-gyp rebuild при наличии в пакете файла binding.gyp, даже если в манифесте package.json не объявлено ни одного сценария жизненного цикла. Злоумышленники воспользовались этим: в их версиях binding.gyp содержит подстановку команды с единственной целью - выполнить node index.js в момент установки. Команда перенаправляет вывод в /dev/null, чтобы не оставлять следов, а затем возвращает фиктивное имя исходного файла, чтобы сама сборка gyp не завершилась ошибкой. Таким образом, атака срабатывает на npm install, а не на импорте, и большинство инструментов, проверяющих сценарии жизненного цикла, не замечают угрозы.
Исследователи Endor Labs в своём отчёте детально разобрали техническую сторону атаки. Файл index.js размером около 4,5 мегабайта содержит обфусцированный код. На верхнем уровне используется упаковщик, который декодирует массив символов с помощью шифра Цезаря (ROT-n) и затем выполняет результат через eval. Примечательно, что ключ сдвига различался между версиями, опубликованными в одну и ту же минуту: у версии 3.8.5 он был 15, а у версии 2.2.1 - 18. Это явная попытка уйти от статических сигнатур, ориентированных на единственную декодированную форму.
После раскрытия первого слоя исполняется асинхронный блок, который использует встроенный модуль Node.js node:crypto для создания экземпляра дешифрования AES-128-GCM. Внутри скрипта зашиты два блока зашифрованного текста: небольшой загрузчик около 900 байт и основной полезный код размером примерно 668 КБ. Загрузчик скачивает автономную среду выполнения Bun (с реального URL-адреса официального репозитория Bun на GitHub, чтобы не вызывать подозрений) и запускает основной payload уже под Bun, а не под Node. Такой приём позволяет обойти средства мониторинга, которые отслеживают только процессы Node.
Сам основной полезный код сильно обфусцирован в стиле obfuscator.io, но анализ встроенных строк показывает, что это многоцелевой сборщик секретов. Он нацелен на креды от основных облачных провайдеров: AWS (токены сессий, ключи доступа, метаданные EC2), GCP (переменная GOOGLE_APPLICATION_CREDENTIALS), Azure (путь к OIDC-токену), HashiCorp Vault (VAULT_TOKEN, заголовки X-Vault-Token), а также на токены Kubernetes, GitHub Actions OIDC, 1Password и Slack. Особую тревогу вызывает сбор токенов реестров npm, GitHub и RubyGems.
Такая комбинация указывает на потенциал самораспространяющегося вредоносного кода. Собрав токены реестров, злоумышленник может публиковать новые вредоносные версии от имени скомпрометированного аккаунта. Учитывая, что четыре вредоносных релиза были выпущены практически одновременно и закрывали все основные линии версий (0.x, 1.x, 2.x, 3.x), атака явно готовила почву для дальнейшего распространения. Это поведение соответствует классу self-replicating supply-chain malware, который иногда называют "Mini Shai-Hulud".
Признаки компрометации достаточно характерны. В установленной версии пакета появляются файлы package/binding.gyp (отсутствует в легитимных сборках) и package/index.js размером 4,5 МБ, хотя точка входа объявлена как ./dist/index.js. Во время установки npm запускает node-gyp rebuild, а вслед за ним - curl и unzip для загрузки Bun. Временная директория в папке ОС может содержать исполняемый файл bun. Разработчикам, использующим ai-sdk-ollama, настоятельно рекомендуется проверить свой dependency tree. Последней чистой версией считается 3.8.4; её хеш указан в оригинальном отчёте. Важно запереть версию в package.json и проверить lock-файлы. На хостах и сборочных серверах, где могла быть установлена заражённая версия, необходимо немедленно отозвать и перевыпустить все собранные токены - особенно токены доступа к облачным провайдерам, реестрам пакетов, GitHub Actions OIDC, Vault, Kubernetes, а также любые. env-переменные и ключи от менеджеров секретов.
Этот инцидент в очередной раз демонстрирует, что угроза со стороны цепочки поставок программного обеспечения остаётся одной из самых труднообнаруживаемых. Использование неочевидного вектора через binding.gyp вместо стандартных install-скриптов позволяет обойти многие автоматические проверки. Обфускация с вращающимися ключами и двухступенчатый запуск через Bun - признаки зрелой и хорошо продуманной кампании. Для защиты на будущее эксперты рекомендуют отключать выполнение сценариев установки по умолчанию (npm install --ignore-scripts), фиксировать версии с помощью хешей целостности, обращать внимание на аномально большие или несоответствующие заявленной точке входа файлы в архиве пакета, а также внимательно отслеживать резкие изменения тега latest и массовые публикации на старых линиях в короткий промежуток времени. Доверие к реестру npm не должно быть слепым, а каждое обновление зависимостей требует вдумчивого аудита.
Индикаторы компрометации
URLs
- github.com/oven-sh/bun/releases/download/bun-v1.3.13/bun-*.zip
Package
- ai-sdk-ollama@0.13.1
- ai-sdk-ollama@1.1.1
- ai-sdk-ollama@2.2.1
- ai-sdk-ollama@3.8.5