Обычная установка зависимостей должна быть скучной. В новой волне ChainDrop вокруг Shai-Hulud опасным стал именно этот будничный момент: runner или рабочая станция разработчика получали скомпрометированный npm-пакет, пакет запускал lifecycle-скрипт во время установки, а среда, где обычно лежат release-токены и cloud-доступы, превращалась в поверхность атаки.

Граф зависимостей с предупреждением, CI runner и защищённые секреты

Что произошло с 4 по 9 августа

Aikido, Socket, SafeDep, JFrog, Unit 42, Wiz, Endor Labs и другие команды описали быструю supply-chain кампанию в npm, связанную с семействами Keyv и Cacheable. По данным Aikido, первый заметный кластер начался с компрометации пути публикации [email protected] и связанных пакетов: flat-cache, file-entry-cache, cacheable-request, cacheable, @cacheable/memory, cache-manager и других. The Hacker News позже описал эту неделю как червеобразную кампанию, которая вышла за пределы исходного пространства имён и затронула сотни package artifacts.

Цифры у источников отличаются не потому, что кто-то обязательно ошибся, а потому что ситуация менялась во время публикаций. JFrog писал о 400+ пакетах, SafeDep и Unit 42 давали более высокие счётчики пакетов или версий, Endor Labs подчёркивал download volume отдельных dependency paths. Эти числа нельзя складывать и нельзя считать количеством жертв. Это оценки затронутых пакетов, версий или объёма загрузок, а не доказательство, что каждая загрузка выполнила вредоносный код.

Почему это не просто ещё один вредоносный пакет

Многие npm-инциденты начинаются с typosquat или почти неизвестного пакета. ChainDrop неприятнее, потому что входная точка включала зависимости, которые команды получают транзитивно. Разработчик может никогда сознательно не выбирать keyv, но получить его через фреймворк, cache layer, линтер, build tool или серверную библиотеку. Поэтому важны lockfiles, а не память инженера: опасная версия может быть на несколько уровней ниже пакета, который команда считает установленным.

Кампания также ударила по привычным сигналам доверия. Исследователи писали, что вредоносные версии публиковались через легитимно выглядящие release workflows и могли иметь valid provenance. Provenance полезен, но отвечает на узкий вопрос: каким workflow собран пакет, под какой identity и из какого repository context. Он не доказывает, что maintainer-аккаунт, release commit, workflow inputs или credentials были честными. Зелёная отметка может показывать аккуратную цепочку происхождения для отравленного релиза.

Как работал путь через install-time код

Публичные отчёты описывают понятную цепочку. Скомпрометированная версия добавляла lifecycle-команду вроде preinstall, которая запускала node setup.mjs. Дальше скрипт подгружал обфусцированный JavaScript; в отчётах встречались имена Math_Symbol.js и math_init.js. Bun использовался как удобный runtime в части сценария. Это не означает, что Bun был скомпрометирован: его использовали как инструмент запуска payload.

Оказавшись внутри CI или рабочей станции, malware искал секреты. JFrog и Unit 42 описывали интерес к npm и GitHub tokens, cloud credentials, Vault и Kubernetes material, SSH keys, .env, Terraform state, Docker и Helm configs, а также временным секретам GitHub Actions runners. Важно не только то, что лежит на диске. Во время job короткоживущие credentials и OIDC-derived tokens могут появляться в окружении или памяти ровно в тот момент, когда install уже выполнился.

Червеобразность возникала из доступа к публикации. Если payload получал npm publishing tokens, он мог публиковать заражённые версии других пакетов жертвы. Если получал GitHub access, исследователи описывали изменения в репозиториях и persistence surfaces: например, файлы для VS Code tasks и Claude Code settings. Не каждый такой файл запускается автоматически, но он показывает намерение атакующих использовать привычную developer automation как следующую точку входа.

Что действительно доказывает provenance

Trusted publishing убирает многие long-lived npm tokens из release setup и привязывает публикацию к CI identity. Это реальное улучшение: потребители и registry получают больше информации о происхождении релиза. Но provenance не является malware scanner. Он не говорит, что maintainer хотел этот change, что source commit прошёл review, что workflow не изменён скомпрометированным аккаунтом, и что lifecycle script безопасно запускать на runner.

Практичная политика должна быть слоистой. Сохраняйте provenance и trusted publishing. Добавляйте branch protection, mandatory review для release workflows, жёсткую защиту maintainer accounts, least-privilege publishing rights и alerts на необычное время релиза. А затем рассматривайте install scripts как privileged code и отдельно решайте, где им разрешено выполняться.

Как понять, затронуты ли вы

Начинайте с доказательств. Ищите точные package names и versions из списков Aikido, SafeDep, Socket, JFrog, Unit 42 и других исследователей в package-lock.json, npm-shrinkwrap.json, pnpm-lock.yaml, yarn.lock, Bun lockfiles, внутренних зеркалах и dependency scanning records. Не останавливайтесь на direct dependencies: transitive dependency как раз и была главной проблемой.

Затем выясните, выполнялся ли install в relevant window. CI logs, timestamps сборок, время создания package-manager cache, container layer history и история команд на рабочих станциях помогут отличить теоретическую зависимость от выполненного кода. Если affected version есть только в lockfile и нигде не устанавливалась, может хватить pinning, update и документации. Если install выполнялся в среде с секретами, считайте эту среду exposed, пока не проверите обратное.

Что делать, если install уже был

Не начинайте с бессмысленного “обновили до latest — закрыли”. Update убирает плохую версию из будущих установок, но не возвращает украденный token. Сначала изолируйте runner или workstation, сохраните нужные logs, остановите использование этой среды для release. Ephemeral runners пересоберите из чистых images. Persistent runners и ноутбуки проверьте на local secret store, shell environment, package-manager cache и изменённую developer tooling.

Credentials лучше ротировать в порядке, который не даст атакующему выпустить доступ заново. Отозвать npm publishing tokens и GitHub tokens, проверить account recovery, maintainers, 2FA и repository permissions. Затем менять cloud keys, registry tokens, Vault tokens, Kubernetes credentials, SSH keys и deployment secrets, которые были доступны среде. Если OIDC token мог быть обменян на cloud access, проверяйте cloud audit trail, а не только CI provider.

Контроли на следующий раз

Главная защита — относиться к install scripts как к privileged execution. Используйте --ignore-scripts, где это совместимо со сборкой, особенно в analysis jobs и dependency-audit шагах. Для пакетов, которым lifecycle scripts действительно нужны, вводите явное approve вместо автоматического выполнения каждого transitive package. Новое поведение npm вокруг approve scripts меняет картину риска, но команды должны протестировать свои версии npm, pnpm, Yarn и Bun, а не считать их одинаковыми.

Помогает и minimum release age для automated installs. Многие supply-chain атаки выигрывают на скорости: poisoned version появляется, automation быстро его resolves, а через несколько часов исследователи уже публикуют предупреждение. Небольшая задержка не остановит targeted attack, но часто превращает live incident в alert, который приходит до production или release CI.

ChainDrop серьёзен, потому что попал туда, где у команд лежат самые удобные секреты: в build path. Это не доказательство, что open source провалился, и не повод панически переписывать стек. Это напоминание: npm install — не просто download. Во многих организациях это выполнение кода рядом с ключами.