Вредоносный npm-пакет: как проверить цепочку зависимостей
Вредоносный пакет может попасть в приложение через транзитивную зависимость или установочный скрипт. Разбираем, как расследовать инцидент в проекте на npm, сохранить улики и ограничить ущерб, не запуская подозрительный код повторно.

Вредоносная зависимость может выполнить код во время установки или уже при запуске приложения — а попасть туда через пакет, который вы напрямую не добавляли. Разбираем инцидент в проекте на npm 10.x: как найти путь пакета, сохранить следы и ограничить ущерб, не превращая расследование в повторный запуск атаки.
Почему пакет из зависимостей становится инцидентом
В npm дерево зависимостей многоуровневое: приложение устанавливает не только пакеты из раздела dependencies в package.json, но и их собственные зависимости. Поэтому подозрительный модуль может появиться глубоко в дереве и не бросаться в глаза при просмотре основного списка.
Есть два разных момента исполнения. Скрипты жизненного цикла пакета могут запускаться при установке. Другой риск — код, который сработает позже, когда приложение импортирует модуль или вызывает его функцию. Отключение установочных скриптов снижает один риск, но не делает пакет безопасным.
Файл package-lock.json фиксирует дерево и контрольные суммы загружаемых архивов. Это помогает воспроизвести установку и заметить незапланированную замену содержимого. Но lock-файл не подтверждает, что зафиксированная версия доброкачественна: если в него попал вредоносный пакет, воспроизводимая установка воспроизведёт и проблему.
Контрольная сумма отвечает на вопрос «тот ли это архив, который зафиксирован в lock-файле», а не «можно ли этому коду доверять».
Транзитивная зависимость* — пакет, который проект получает не напрямую, а через другую зависимость.
Сначала ограничьте ущерб и сохраните следы
Не начинайте расследование с обычной переустановки зависимостей на рабочей машине. Она может повторно запустить установочные скрипты, изменить lock-файл, очистить полезные следы или передать управление подозрительному коду.
- Остановите новые сборки и публикации. Приостановите затронутый CI job и развёртывание, пока не станет понятен масштаб. Сохраните логи сборки, артефакты, текущий package.json и package-lock.json.
- Проверьте, какие секреты были доступны. Учитывайте переменные CI, токены публикации, ключи доступа и учётные данные, доступные приложению. Если пакет мог прочитать секрет, отзовите его и выпустите новый с чистой машины. Простое удаление пакета не отменяет уже возможную утечку.
- Работайте с копией проекта в изоляции. Не запускайте подозрительное приложение и не подключайте копию к production-секретам. Для первичного осмотра можно установить дерево с отключёнными установочными скриптами; сеть и права окружения всё равно ограничьте.
- Установите временной диапазон. Сопоставьте первый коммит или lock-файл с подозрительной версией, логи CI и даты развёртываний. Зафиксируйте, какие сборки и среды могли получить этот пакет.
Сохраните исходные файлы отдельно и не перезаписывайте их результатами расследования. Если затронута инфраструктура, привлеките ответственных за безопасность: очистка только локального проекта не доказывает, что секреты и серверы не пострадали.
Как найти пакет в npm-дереве
Команды ниже предназначены для npm CLI 10.x и проекта с package-lock.json; для lock-файла формата 3 это типичная связка. Выполняйте их в изолированной копии, а результаты сохраняйте отдельно от исходных материалов.
git diff -- package.json package-lock.json
npm ci --ignore-scripts
npm ls --all
npm explain ИМЯ_ПАКЕТАПервая команда показывает изменения в манифестах относительно Git. Вторая устанавливает зафиксированное дерево, не выполняя установочные скрипты; она не отключает код, который приложение может выполнить позднее. Третья выводит установленное дерево, а четвёртая объясняет, почему указанный пакет попал в проект. Замените ИМЯ_ПАКЕТА на точное имя подозрительной зависимости.
Если npm ci сообщает об ошибке или несовпадении package.json и lock-файла, не исправляйте файлы командой обновления до сохранения копий. Несоответствие может быть следствием обычной незаконченной работы, но в расследовании важно отличить его от изменений, внесённых после инцидента.
Что именно сравнивать
- Неожиданные изменения имени, версии, источника или контрольной суммы пакета в package-lock.json.
- Изменения scripts в package.json пакета, особенно новые команды, которые выполняются при установке.
- Появление зависимости после обновления другого модуля, даже если её нет в вашем package.json.
- Разницу между lock-файлом в подозрительной сборке и версией из доверенного коммита или артефакта.
Если выяснилось, что пакет пришёл транзитивно, npm explain показывает цепочку родителей. Это даёт конкретную точку для исправления: иногда достаточно обновить или заменить родительскую зависимость, а не удалять полдерева вручную.
Чего не доказывает npm audit
npm audit сопоставляет дерево зависимостей с известными сообщениями об уязвимостях. Это полезная проверка, но не детектор вредоносного поведения: новый пакет, компрометированная версия или код, которого ещё нет в базе advisories, могут не появиться в отчёте. Чистый результат не означает, что зависимость безопасна.
И наоборот, найденная уязвимость не доказывает, что её использовали для атаки. Разделяйте три вопроса: есть ли известная уязвимость, мог ли пакет попасть в сборку и успел ли его код выполниться в среде с ценными доступами.
Проверка сигналов: что дают разные источники
| Источник | Что помогает выяснить | Чего не подтверждает |
|---|---|---|
| Git-история и lock-файл | Когда и каким изменением появилась версия, изменились ли источник и контрольная сумма | Что пакет безопасен или не запускался |
| npm ls и npm explain | Есть ли пакет в установленном дереве и через какую зависимость он пришёл | Что его код исполнился в production |
| Логи CI и развёртываний | Какая сборка получила пакет, какие команды запускались, какие переменные были доступны | Что в логах отражены все действия процесса |
| npm audit | Известные базе npm advisories для дерева зависимостей | Отсутствие вредоносного кода или компрометации |
Ищите согласованность между источниками. Подозрительная версия в lock-файле вместе с успешной сборкой показывает потенциальное присутствие пакета, но не доказывает утечку. Для вывода об исполнении нужны данные о запуске скриптов или приложения, а для оценки ущерба — сведения о доступных секретах и сетевых соединениях.
Где расследование даёт ложную уверенность
- Пакет отсутствует сейчас. Он мог находиться в более раннем lock-файле или артефакте. Проверяйте историю сборок, а не только текущую ветку.
- Установочные скрипты отключены. Это не блокирует исполнение обычного кода при запуске приложения и не очищает уже заражённый образ.
- Контрольная сумма совпала. Она подтверждает соответствие архиву, записанному в lock-файле. Если изменение lock-файла и было способом внести пакет, совпадение ничего не говорит о намерениях автора.
- Сканер ничего не нашёл. Автоматическая проверка зависит от доступных ей правил и данных. Для неизвестного поведения нужны изоляция, анализ изменений и проверка следов в среде выполнения.
Что измерять: не абстрактное количество проверок, а время от появления подозрительной версии до остановки сборок, число артефактов и окружений, в которых она могла оказаться, и перечень секретов, доступных этим окружениям. Сравнивайте эти данные между релизами; универсальных безопасных значений для них нет.
Что делать дальше
После локализации удалите или замените затронутую зависимость, проверьте lock-файл из доверенного источника и пересоберите приложение в чистом окружении. Не переносите старые node_modules и образы в новую сборку. Если секреты были доступны подозрительному коду, замените их независимо от того, обнаружили ли вы подтверждение утечки.
Затем ограничьте права CI до необходимых для сборки и публикации, отделите секреты production от обычных pull request и проверяйте изменения lock-файла в ревью. Следующий практический шаг — пройти путь одной зависимости от package.json до CI-артефакта и выяснить, на каком этапе вы сможете остановить подозрительную версию до её развёртывания.
* Анализ состава программного обеспечения, или SCA, — проверка проекта на известные уязвимости и лицензии его прямых и транзитивных зависимостей. Сам по себе SCA может не выявить новый вредоносный пакет.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: npm CLI 10: npm ci, npm CLI 10: npm audit
Частые вопросы
Если npm audit ничего не нашёл, значит ли это, что пакет безопасен?
Достаточно ли выполнить npm ci --ignore-scripts?
Что делать, если подозрительный пакет уже попал в production?
Подозрительный пакет пришёл через другую библиотеку. Как его найти?
Что делать, если у меня другая версия npm или lock-файла?
Совпадение контрольной суммы доказывает, что архив безвредный?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.