Мы все живём на чужом node_modules
30 августа 2026 г.
6 мин
1

Мы все живём на чужом node_modules

Есть один момент, который я стараюсь не додумывать до конца, потому что после него плохо спится.

Вот я запускаю npm ci на новом проекте. Через сорок секунд в папке лежит тысяча с лишним пакетов. Я не читал код ни одного из них. Я не знаю в лицо ни одного автора. Я не проверял, кто именно вчера вечером выложил новую патч-версию вон той библиотеки на три строчки, которая парсит даты. Я просто доверяю. Все доверяют. На этом доверии, если честно, держится вся современная веб-разработка - и мы обычно называем это «экосистемой», хотя правильнее было бы называть «цепочкой поручительств, где никто никого не проверял».

4 августа эту цепочку дёрнули за конец.

Что произошло

Червь, который сам себя называет Shai-Hulud (исследователи зовут его ChainDrop), скомпрометировал репозиторий keyv - и через учётку мейнтейнера расползся дальше. Итог: больше 400 пакетов и свыше 1700 версий. Среди них не экзотика, а вещи, которые лежат у половины из нас:

  • keyv — 600 миллионов скачиваний в месяц
  • flat-cache — 580 миллионов
  • cacheable-request — 137 миллионов
  • cache-manager — 16 миллионов

Суммарно - больше 1,3 миллиарда установок в месяц. Это не «уязвимость в библиотеке». Это как если бы кто-то подмешал что-то в водопровод, и вопрос был только в том, пил ли ты воду на этой неделе.

Механика простая до обидного. В package.json прописывается хук preinstall, который запускает node setup.mjs. Скрипт скачивает рантайм Bun, выполняет обфусцированный пейлоад - и дальше начинается самое интересное. Никакого «злоумышленник получил доступ». Ты сам, своими руками, набрал npm i и запустил чужой код с правами своего пользователя. До того, как хоть что-то установилось.

Почему это не обычная CVE

Обычная уязвимость - это дырка. Ты её закрываешь, и всё.

Здесь - червь. Разница принципиальная: он размножается сам. Собрав с твоей машины npm-токены с правом записи (особенно те, у которых включён bypass_2fa - привет, автоматизация в CI), он идёт и делает ровно то, что сделал бы ты: скачивает тарбол своего же пакета, подмешивает туда себя, поднимает патч-версию и публикует. Каждый заражённый разработчик становится следующей волной. Границы такой атаки не существует - её нельзя очертить списком, потому что список растёт сам.

И собирает он не только npm-токены. Список того, что вычищает коллектор: AWS, Azure, GCP, Hetzner, секреты Kubernetes, конфиги Helm, KV-хранилища Vault, SSH-ключи, VPN-профили, браузерные хранилища, /etc/shadow. Больше трёхсот паттернов.

Отдельным пунктом - токены Anthropic, OpenAI, Cursor, Codex, Gemini. Вот это, пожалуй, самая примета 2026 года. Раньше воровали доступ к деньгам и инфраструктуре. Теперь воруют доступ к твоим ИИ-инструментам - потому что у них, во-первых, есть биллинг, а во-вторых, они умеют писать код в твоих репозиториях. Червь, кстати, этим и пользуется: коммитит вредоносные хуки в ветки (до пятидесяти штук) с невиннейшим сообщением chore: update config и подписью автора claude@users.noreply.github.com. Расчёт на то, что ты пролистаешь такой коммит не глядя. Признайтесь, пролистали бы.

Отдельно ценю чувство прекрасного у авторов: адрес C2-сервера в коде не зашит вообще - вредонос ходит за ним в смарт-контракт в мейннете Ethereum. Такое не заблокируешь на файрволе. А репозитории, которые он создаёт, помечены описанием «Shai-Hulud: Here We Go Again». Ребята явно получают удовольствие от работы.

Тут я должен признаться

У нас несколько живых проектов. Внутренний Gitea, дев-прокси, десяток фронтендов на Next.js, бэкенды на Go и Node. И ни одного человека, чья работа - смотреть, что приехало в зависимостях.

Я это знал и до августа. Просто раньше это было абстрактным риском из чек-листа, а не новостью в ленте.

Тем более что мне уже прилетало - правда, с другой стороны. Не так давно к нам в Gitea заехали через известную дыру в внутреннем API и уронили дроппер майнера. Повезло: пейлоад не отработал. Я тогда закрыл внутренние эндпоинты на nginx, привязал Gitea к 127.0.0.1, прокрутил SECRET_KEY и INTERNAL_TOKEN, обновился с 1.24.7 на 1.26.4 и почувствовал себя молодцом.

А потом прочитал разбор ChainDrop и понял: против такого сценария всё, что я сделал, не сработало бы вообще никак. Потому что там атакующий ломился снаружи внутрь - это привычно, от этого есть периметр. А тут вредонос заезжает через парадный вход, в обнимку с легитимным обновлением, которое я сам скачал, и с правами моего же разработчика. Периметр в этой схеме не участвует. Он даже не в курсе, что что-то произошло.

Что я поменял (и что советую посмотреть у себя)

Без иллюзий: полностью от этого класса атак не закрыться. Но снизить вероятность - вполне.

Первое: npm 12+. Там preinstall-хуки по умолчанию не выполняются. Это буквально отключает основной вектор запуска. Если у вас в CI прибит гвоздями Node постарше - это отличный повод наконец обновиться.

Второе: --ignore-scripts как значение по умолчанию. В .npmrc, глобально, для всех сборочных агентов. Да, часть пакетов сломается - те, что реально требуют компиляции. Их окажется меньше, чем вы боитесь. И вот их-то можно вносить в белый список руками и осознанно.

Третье: убить долгоживущие токены с правом записи. Это то, чем червь размножается. bypass_2fa-токены в CI - самое уязвимое место, какое можно придумать. Trusted publishing через OIDC вместо вечного секрета в переменных окружения.

Четвёртое: карантин на обновления. Не тянуть latest в тот же день, когда он вышел. Неделя выдержки ловит почти все такие истории - их обычно обнаруживают за часы, а не за месяцы. Это скучно, это не про «мы на последней версии», зато работает.

Пятое: пройтись по своим репозиториям грепом. Ищите коммиты с сообщением chore: update config, автора claude@users.noreply.github.com, файлы .claude/settings.json и .vscode/tasks.json, которых вы не создавали, и воркфлоу, которые сериализуют ${{ toJSON(secrets) }}. Пятнадцать минут работы. Лучше потратить их и ничего не найти.

Шестое, самое неприятное: если попали - ротировать всё. Не «подозрительные» токены, а все. Пересобрать раннеры и рабочие машины с чистых образов. Полумеры тут не работают: червь уже унёс то, что унёс, и ждёт.

Что в сухом остатке

Меня во всей этой истории цепляет не техника. Техника как раз понятная.

Цепляет то, что мы построили индустрию, в которой нормальный, ответственный, опытный разработчик каждый день запускает у себя на машине тысячи строк кода от людей, которых он никогда не видел - и это считается не безрассудством, а профессиональным стандартом. Мы называем это «не изобретать велосипед». И это правда экономит колоссальное количество времени. Ровно до того дня, когда не экономит.

Я не собираюсь писать свой keyv. Вы тоже не собираетесь, и это разумно. Но, кажется, стоит перестать делать вид, что npm install - это безобидная команда установки. Это команда «выполнить произвольный код от лица моего пользователя, доверившись цепочке незнакомцев». Просто раньше нам это сходило с рук.

Обновите npm. Отключите скрипты. Выкиньте вечные токены. И сходите проверьте свои репозитории - прямо сейчас, пока не закрыли вкладку.