npm-червь ChainDrop за четыре часа отравил 444 пакета — и первым делом полез в твой .claude/settings.json
4 августа в 09:02:37 UTC в репозиторий jaredwray/keyv прилетел коммит ee2681a с подписью «release: v6.0.0». Через четыре часа заражённых пакетов было 444, а суммарная недельная загрузка головных трёх — под 450 миллионов. Но интересно другое: этот червь первым делом ищет не твои AWS-ключи. Он ищет папку .claude.
TL;DR: Червь ChainDrop (потомок Shai-Hulud 2.0) стартовал с
keyv@6.0.0и за неполные четыре часа переопубликовал сам себя в 444 пакета и 2212 версий. Он крадёт credential-сторы Claude, Codex, Cursor, Gemini и OpenAI, дописывает свои хуки в.claude/settings.jsonи.vscode/tasks.json, а вредоносные релизы вышли с валидной SLSA-провенансом через npm OIDC Trusted Publishing. И ещё он завершается на системах с русской локалью.
Что произошло с keyv и почему это касается всех
Атакующий получил доступ к GitHub-аккаунту Джареда Рэя, мейнтейнера пакета keyv (свыше 150 млн загрузок в неделю), и запушил отравленный коммит прямо в main. Дальше сработала штатная релизная автоматизация самого проекта.
Коммит ee2681a делал три вещи: подкладывал в core/keyv файлы setup.mjs и Math_Symbol.js (727 680 байт обфусцированного второго стейджа), добавлял фальшивый прикрывающий тест и переписывал релизный тулинг так, чтобы v6.0.0 ушла под dist-тегом latest. То есть любой, кто поставил keyv без пина версии, получил малварь.
Из аккаунта Рэя утекли не только keyv. По разбору StepSecurity в первой волне было 11 подтверждённых полных носителей червя: cacheable, flat-cache (149,9 млн загрузок в неделю), file-entry-cache (147,6 млн), cacheable-request, cache-manager, ecto и другие. flat-cache и file-entry-cache — это зависимости ESLint. Если у тебя в проекте есть линтер, ты в графе зависимостей.
Вторая волна пошла уже без человека. Заполучив npm-токен с правом публикации, пейлоад перебирает все доступные этому аккаунту пакеты, скачивает свежий тарбол, вклеивает себя, добавляет preinstall-хук, инкрементит патч-версию и публикует обратно. К 13:20 UTC так набралось ещё 433 пакета в 2201 версии — уже из чужих скоупов: @deliveroo/reevent, @or-sdk/invitations, @picsart/ai-sdk, @qlik/embed-runtime, picasso.js. По подсчёту CSO Online пострадали пакеты организаций Deliveroo, Ornikar, OneReach, Picsart, Qlik и ServiceTitan, суммарно больше 2 миллиардов загрузок в месяц.
Цифры у разных исследователей разные, и это нормально для живого инцидента: LWN на утро 4 августа писал про 435 пакетов и 1550 версий, StepSecurity к 18:10 UTC насчитал 444 и 2212, BleepingComputer говорил про «более 1300». Считай любую конкретную цифру снимком на момент публикации, а не финалом.
Почему валидная SLSA-провенанс не спасла
Потому что провенанс подтверждает, из какого коммита собран пакет, но не подтверждает, что этот коммит был авторизован. Головные пакеты вышли через npm OIDC Trusted Publishing с корректными SLSA-аттестациями, и ни одна автоматическая проверка провенанса их не отличила бы от легитимного релиза.
Это ключевое отличие от компрометации axios в марте 2026: там отсутствие провенанса как раз и было признаком подделки. Здесь атакующий не подделывал подпись — он захватил аккаунт, запушил коммит и дал релизному workflow проекта подписать малварь своими руками. Криптография отработала штатно и подтвердила ровно то, о чём её спросили.
Отдельная деталь, которую стоит переварить: Рэй подтвердил в X, что использовал OIDC вместо долгоживущих npm-токенов и одноразовые коды для GitHub. То есть он делал ровно то, что советуют все гайды по безопасности supply chain. И это не помогло, потому что скомпрометирован был сам аккаунт, а не токен.
Кстати, у пакетов второй волны провенанса как раз нет — Microsoft отмечает, что многим вредоносным версиям не соответствует ни коммит, ни PR, ни тег: червь правил тарболы напрямую. Так их и отличают: вторая генерация носит файл math_init.js вместо Math_Symbol.js.
Как ChainDrop охотится на AI-инструменты
Он воспринимает твой агент как отдельный класс добычи: крадёт credential-сторы Claude, OpenAI, Codex, Cursor и Gemini, а конфиги агентов использует как механизм персистентности.
По анализу Wiz список целей для кражи вырос примерно на 70% против предыдущих версий Shai-Hulud. Добавились как раз credential-сторы AI-агентов, а вместе с ними криптокошельки Foundry, Solana и Monero, секреты self-hosted CI (master.key Jenkins, Argo CD, Harbor), конфиги Alibaba Cloud и Tencent Cloud CLI и /etc/shadow.
Дальше начинается то, чего в предыдущих npm-червях не было. Найдя токен вида ghs_, пейлоад коммитит себя во все ветки, до которых достаёт: до 50 веток на репозиторий, начиная с самых свежих, пропуская ветки dependabot и copilot (чтобы не отсветиться в автоматических PR). В каждую он дописывает вредоносные хуки в .claude/settings.json и .vscode/tasks.json.
Смысл этого хода: заражение перестаёт зависеть от npm. Коллега клонирует репозиторий, открывает его в редакторе или запускает Claude Code — и хук выполняется у него на машине. Microsoft прямо называет это «дополнительным путём заражения от разработчика к разработчику».
И самое неприятное: червь ставит вотчер, который отстреливает пейлоад атакующего в момент, когда украденный GitHub-токен отзывают. Отзыв токена, то есть стандартная первая реакция на инцидент, здесь сам работает триггером.
Ещё один штрих для нашей аудитории. На старте пейлоад делает preflight-проверку окружения и завершается на системах с русским языком. Причина не благотворительность, а география авторов: приём давно известен по семейству вымогателей и служит защитой от исследователей и от юрисдикционных проблем. Практический вывод из этого один: русская локаль — это не защита, а всего лишь причина не увидеть срабатывание у себя и спокойно протащить заражённый lock-файл в CI, где локаль английская.
Технически исполнение банальное: preinstall-хук скачивает легитимный рантайм Bun и им запускает обфусцированный бандл. Bun выбран не для скорости, а чтобы обойти мониторинг, заточенный на Node. Эксфильтрация — AES-256-GCM на HTTPS-эндпоинт npm-cache[.]com, с резервным каналом через GitHub-репозитории с описанием «Shai-Hulud: Here We Go Again». Управление — через Ethereum-блокчейн (техника EtherHiding), поэтому C2 нельзя просто снести по домену.
Подводные камни
preinstallвыполняется до установки. npm запускает хук раньше, чем установка завершится, то есть раньше любых тестов и обычных проверок безопасности. К моменту, когда твой CI-пайплайн дошёл бы до сканера, креды уже уехали.- Удалить пакет недостаточно. Всё, что было доступно с машины на момент запуска, надо считать утёкшим и ротировать — включая доступы к другим машинам, до которых с этой можно дотянуться. Ротацию делай с чистого хоста: если ты генерируешь новый токен на заражённой машине, ты просто отдаёшь атакующему свежий.
- Ротация токена триггерит пейлоад. Вотчер на отзыв
ghs_-токена означает, что порядок действий важен: сначала изолируй машину, потом отзывай. Наоборот — хуже. - Пакеты сидят транзитивно.
flat-cacheиfile-entry-cacheприезжают через ESLint, а не через твойpackage.json. Проверять надо lock-файл и SBOM, а не список прямых зависимостей. - Провенанс-чек в CI даёт ложное спокойствие. Если у тебя стоит гейт «пускаем только пакеты с валидной аттестацией» — головные версии ChainDrop он пропустил бы.
- Аккаунт мейнтейнера удалён вместе с историей. GitHub-аккаунт Рэя и три исходных репозитория недоступны, поэтому upstream-треды, где инцидент обсуждался, больше не прочитать. Если ты не сохранил себе список версий, восстанавливать придётся по сторонним отчётам.
Альтернативы: чем это ловится
min-release-ageв npm CLI v12 — карантин на свежие релизы. Microsoft рекомендует именно это, и в данном случае мера работает: окно между публикацией и обнаружением было около четырёх часов, любой ненулевой cooldown его бы перекрыл. Самая дешёвая защита из существующих.--ignore-scriptsпо умолчанию — вырубает весь векторpreinstallцеликом. Ломает пакеты с нативными сборками, поэтому в реальности живёт как allowlist на конкретные зависимости, а не как глобальный флаг.- Прокси-реестр с блок-листом (StepSecurity Secure Registry, Artifactory, Verdaccio с политиками) — режет известные плохие версии на скачивании,
npm install keyv@6.0.0падает закрыто. Минус в том, что защита реактивная: пока версия не помечена, она проходит. - Пиннинг и
npm ci— не защита от атаки, а защита от её распространения: lock-файл не даст молча подтянуть новый патч. Именно поэтому инкремент патч-версии и был выбран червём как способ размножения.
Вердикт
Проверяй экспозицию сегодня, даже если считаешь, что keyv у тебя нет — при 450 млн загрузок в неделю у трёх головных пакетов и заражении через ESLint интуиция тут не работает, работает только грепанье lock-файлов. Если хоть одна из версий ставилась в окне 4 августа с 09:00 до примерно 18:00 UTC, считай машину и все доступные с неё креды скомпрометированными и ротируй с чистого хоста, а не с этой машины.
А вот вывод посерьёзнее: ChainDrop — первый крупный червь, который относится к конфигу AI-агента как к точке персистентности, а к его токенам как к отдельной добыче. Значит .claude/settings.json, .vscode/tasks.json и подобные файлы пора считать исполняемым кодом и ревьюить в PR соответственно. Гейт «только пакеты с валидным провенансом» после этого инцидента можно оставить, но полагаться на него как на границу доверия больше нельзя.
Как попробовать проверить себя
- Грепни lock-файлы на головные пакеты и версии:
grep -rE "keyv|flat-cache|file-entry-cache|cacheable|cache-manager" package-lock.json yarn.lock pnpm-lock.yaml. Полный список 444 пакетов и версий — в отчёте StepSecurity, сопоставление с твоими репозиториями есть в базе Snyk. - Поищи следы пейлоада на диске и в репозиториях: файлы
setup.mjs,Math_Symbol.js,math_init.js, а также"preinstall": "node setup.mjs"в любомpackage.jsonвнутриnode_modules. - Проверь свои агентские конфиги на незапрошенные хуки:
git log -p --all -- .claude/settings.json .vscode/tasks.json. Это же стоит прогнать по всем ветками репозиториев, куда у тебя есть push-доступ. - Обнови npm CLI до v12 и включи карантин на свежие релизы — по гайду Microsoft это ключевая мера. Заодно почисти кеши npm и yarn на всех затронутых машинах.
- Если нашёл совпадение: сначала изолируй машину от сети, только потом отзывай токены (помни про вотчер), и ротируй всё с заведомо чистого хоста.