{ "index": 169, "slug": "editorial-2023-04-field-dependency-security", "title": "Безопасность зависимостей: как проверить транзитивный пакет до релиза", "excerpt": "Сканер нашёл пакет, которого нет в package.json. Разбираем путь через npm-дерево, lockfile, production-артефакт и runtime, затем проверяем обновление и откат без ложного PASS.", "contentHtml": "

Сканер сообщает об уязвимой версии пакета, но в package.json этого имени нет. Команда удаляет случайную строку, получает зелёный install и закрывает задачу. Позже выясняется, что пакет пришёл транзитивно, остался в другом production-артефакте или уже был упакован в образ. Обратный сценарий тоже опасен: обновление без проверки меняет peer-зависимость, требует другой Node.js и останавливает деплой.

\n

Проверяемый вывод должен быть уже: какой пакет найден, кто его привёл, в каком артефакте он оказался, достигается ли уязвимая ветка в названном сценарии и можно ли вернуться к исходному digest. Предупреждение scanner — вход в расследование, а не доказательство ни безопасности, ни эксплуатации.

\n

Не начинайте с удаления строки

\n

Зависимость может отсутствовать в корневом манифесте и всё равно входить в установленное дерево. Приложение зависит от http-client, тот — от parser, а advisory указывает на старую версию parser. Нужно проверить не только имя и версию, но и путь от production root до компонента.

\n

Есть и обратная граница. Запись в lockfile описывает результат разрешения, но не доказывает, что пакет физически попал в конкретный образ. Сборка может исключить dev-зависимость, заменить optional-ветку, собрать другой workspace или использовать другой lockfile. Поэтому после дерева проверяют именно выходной артефакт и его digest.

\n

Четыре слоя доказательств

\n

Разделите объекты до первого изменения. Манифест описывает намерение проекта и диапазоны прямых зависимостей. Lockfile фиксирует разрешённое дерево. SBOM (Software Bill of Materials) перечисляет компоненты и отношения поставки для названного артефакта. Runtime evidence показывает наблюдение конкретного процесса и сценария. Каждый слой отвечает на свой вопрос и не заменяет соседний.

\n

Сначала зафиксируйте baseline: commit, package manager, Node.js, настройки .npmrc, команду установки и digest текущего артефакта. Затем назовите candidate: пакет, исходную и целевую версии, advisory и предполагаемый путь обновления. Без baseline нельзя отличить удаление уязвимого узла от его перемещения в другую ветку.

\n
Что именно проверяет каждый слой
СлойВопросЧто сохранить
package.jsonКакие direct dependencies и диапазоны объявлены?Commit и diff манифеста
package-lock.jsonКакое дерево разрешено зафиксированным проектом?Полный diff, integrity и настройки resolver
node_modules / imageЧто физически установлено в проверенном артефакте?Имя образа, digest и состав слоя
SBOMКакие компоненты заявлены для named artifact?Формат, источник генерации и связь с digest
RuntimeЧто произошло в названном entry point?Команду, вход, ответ, лог и отрицательный путь
\n

Воспроизводимый маршрут для npm

\n

Ниже команды для проекта на npm. Подставьте в PACKAGE имя из advisory и выполняйте их в том workspace, который собирается в production. Если pipeline использует --legacy-peer-deps, workspaces или иной .npmrc, повторите те же настройки: разрешение зависит не только от текста манифеста.

\n
PACKAGE=parser\n\nnode --version\nnpm --version\nnpm ci\nnpm ls \"$PACKAGE\" --all --omit=dev --json > /tmp/dependency-tree.json\nnpm explain \"$PACKAGE\"\nnpm audit --omit=dev --json > /tmp/npm-audit.json
\n

npm ci — контрольная точка. По официальной документации ему нужен существующий lockfile; при расхождении с package.json команда завершается ошибкой, удаляет имеющийся node_modules и не переписывает манифест или lockfile. Если установка упала, это отдельный результат: сначала разберите peer-диапазоны, версию npm и флаги, с которыми lockfile был создан.

\n

Не заменяйте npm ci на npm install ради зелёного exit code: обычная установка может пересчитать lockfile. Флаг --omit=dev убирает dev-зависимости с диска, но записи о них остаются разрешёнными в lockfile. Если build использует dev-инструменты, описывайте build-артефакт отдельной проверкой.

\n

npm ls --all --omit=dev --json показывает логическое дерево установленных пакетов и может отметить missing, invalid или extraneous узлы. npm explain даёт обратный путь — почему пакет присутствует. Нулевой вывод не закрывает advisory, пока не проверены правильные workspace, lockfile, режим установки и artifact.

\n

Найдите каждый путь в дереве

\n

Одна строка parser@1.0.0 отвечает только на вопрос о версии. Один пакет может присутствовать несколько раз из-за несовместимых диапазонов. Логическое дерево npm также не равно физическому расположению на диске: deduplication и peer-зависимости меняют картину. Для решения нужны все пути от корня.

\n
function findPaths(node, wanted, path = []) {\n  const name = node.name || '<root>';\n  const version = node.version || 'unknown';\n  const current = path.concat(name + '@' + version);\n  const matches = node.name === wanted\n    ? [{ version: version, path: current }]\n    : [];\n\n  for (const entry of Object.entries(node.dependencies || {})) {\n    const childName = entry[0];\n    const child = entry[1];\n    matches.push(...findPaths(\n      Object.assign({ name: childName }, child),\n      wanted,\n      current,\n    ));\n  }\n\n  return matches;\n}\n\nconst tree = JSON.parse(require('fs').readFileSync(0, 'utf8'));\nconst packageName = process.argv[2] || 'parser';\nconsole.log(JSON.stringify(findPaths(tree, packageName), null, 2));
\n

Сохраните фрагмент как find-paths.js и выполните node find-paths.js parser < /tmp/dependency-tree.json. Учебный вывод может показать две цепочки — через http-client@4 и через markdown-tool@2. Это доказывает две записи в переданном JSON, но не доказывает, что обе попали в image и что опасная ветка вызывается. Учитывайте это ограничение при формулировке результата.

\n

Сопоставьте дерево с артефактом

\n
Схема границ manifest, lockfile, SBOM и runtime evidence: каждый следующий слой требует отдельной проверки и не является автоматической гарантией предыдущего
Manifest выражает намерение, lockfile — разрешённое дерево, SBOM — состав названного артефакта, runtime evidence — наблюдение конкретного сценария. Стрелки означают проверку связи, а не доказанную безопасность.
\n

SBOM полезен только вместе с идентичностью сборки: commit, digest, временем и понятным источником генерации. Файл с названием sbom.json без связи с release candidate может относиться к соседнему образу. Сверяйте компонент, версию, источник и relationship; расхождение с lockfile — сигнал расследования, а не повод выбрать более удобный результат.

\n

Сканируйте immutable digest именно публикуемого образа или архива. Локальный node_modules, staging-образ и предыдущий digest не являются заменой. Если сборка создаёт несколько image layers, выясните, где лежит компонент и не остаётся ли старая копия в другом слое.

\n
Типовой симптом и следующий тест
СимптомГипотезаПроверкаДействие
Пакета нет в package.jsonОн транзитивный или пришёл из другого workspacenpm ls --all и npm explain в production rootНазвать родителя и проверить каждый путь
Lockfile изменился, advisory осталсяОсталась другая копия или веткаСравнить все записи и версии в дереве и SBOMОбновить каждого владельца либо обосновать исключение
npm ci зелёный, сервис не стартуетНе совпали Node.js, peer range, ABI или module formatSmoke в том же image и runtimeОстановить выпуск и сузить candidate
Сканер не видит старую версиюПроверен другой digest или слойСопоставить digest отчёта и releaseПересканировать публикуемый artifact
Rollback вернул версию, но данные не читаютсяИзменился формат или внешний протоколПроверить обратную совместимость миграцииРазделить security update и изменение данных
\n

Отделите наличие от достижимости

\n

Наличие пакета и достижимость уязвимого кода — разные утверждения. Назовите entry point, импорт или вызов, входные данные и режим исполнения. Для сервера это HTTP-обработчик, для CLI — команда, для worker — сообщение из очереди. Формулировка «ветка не достигнута в проверенном сценарии» честнее, чем «уязвимости нет».

\n

Проверьте optional-зависимости на целевой платформе, peer-зависимости, bundled packages, worker, cron и отдельный CLI. Сборщик может исключить статический импорт, а динамический require сохранить путь. Строковый поиск находит имя, но не учитывает условие, экспорт, bundler и конфигурацию.

\n

Минимальное evidence — связка dependency path, содержимого production-артефакта и названного сценария с ожидаемым результатом. Добавьте отрицательный путь: некорректный вход должен быть отклонён ожидаемым способом, а недоступная optional-часть не должна молча создавать видимость исправности.

\n

Обновление как контролируемое изменение

\n

Сформируйте небольшой candidate. Обновляйте прямого родителя, если он выпускает совместимую версию, или добавляйте явное разрешение с объяснением владельца риска. Не меняйте одновременно Node.js, package manager и несколько крупных библиотек: rollback перестанет показывать причину отказа.

\n

Прочитайте весь diff lockfile: resolved URL, integrity, peer и optional-поля, количество копий и новые install scripts. Поле engines выражает заявленный диапазон совместимости. Без engine-strict npm может оставить предупреждение и продолжить, а строгая проверка всё равно не запускает приложение и не проверяет native addon.

\n

Повторите clean install с параметрами pipeline. Затем запустите startup, импорт компонента, валидный и невалидный вход, сборку, сетевой вызов и затронутый CLI. У каждого теста должны быть команда, окружение и exit code. Фраза «всё прошло» без этого набора не является доказательством.

\n

Порядок действий перед выпуском

\n
  1. Скопируйте advisory: пакет, затронутый диапазон, исправленную версию и источник. Отделите утверждение advisory от гипотезы о вашем приложении.
  2. Определите production root и baseline: commit, lockfile, Node.js, npm, .npmrc и digest текущего артефакта.
  3. Выполните npm ci, затем сохраните npm ls --all --omit=dev --json и npm explain PACKAGE. Разберите каждый путь и копию.
  4. Соберите candidate с минимальным diff. Проверьте манифест, lockfile, integrity, peer/optional-ветки и install scripts.
  5. Повторите clean install и тесты на том же runtime. Проверьте успешный и отрицательный сценарий затронутого entry point.
  6. Соберите image или архив. Сканируйте его immutable digest и сопоставьте результат с SBOM, lockfile и commit.
  7. Сделайте runtime smoke в том же окружении: старт, безопасный запрос, ожидаемый отказ и нужные worker/CLI paths.
  8. Проверьте rollback к baseline. При изменении формата данных, миграции или протокола отдельно подтвердите обратную совместимость.
\n

Ложные зелёные результаты

\n

Зелёный npm audit не означает, что внешний scanner ошибся: базы advisory, области установки и правила инструментов различаются. Красный scanner тоже не доказывает достижимость вызова. Зафиксируйте источник, версию базы, путь пакета и artifact, затем согласуйте решение с владельцем риска.

\n

Установка с --ignore-scripts не проверяет install script, который нужен приложению. macOS-проверка не заменяет Linux-образ, если native dependency собирается в CI. Локальная папка не заменяет image digest. Эти ограничения пишутся рядом с результатом, иначе отчёт создаёт ложную уверенность.

\n

Ограничения применимости

\n

Маршрут рассчитан на npm-проекты с package-lock.json. Для Yarn, pnpm, Cargo, Maven или системных пакетов команды и формат lockfile будут другими, хотя разделение manifest, resolved tree, artifact и runtime остаётся полезной моделью. Не переносите npm-команды в другой менеджер без сверки его документации.

\n

Статья не определяет exploitability и не заменяет threat model, code review или расследование инцидента. Достижимость зависит от кода, конфигурации, прав, входных данных и внешних сервисов. Если не удалось получить точный digest, выполнить сценарий или сопоставить SBOM с build, статус должен быть unknown, а не safe.

\n

Major-обновление требует отдельной оценки API-изменений. Для native addon добавьте проверку ABI и целевой платформы. Для private registry сохраняйте provenance, но не публикуйте токены и приватные URL в отчёте.

\n

Критерий готовности

\n

Обновление готово к выпуску, когда одна связанная запись содержит advisory и scope, baseline и candidate, все dependency paths, diff lockfile, clean install, тесты затронутого контракта, runtime smoke, SBOM или иной состав артефакта, сканирование того же digest и проверяемый rollback. У каждого результата есть команда, окружение и exit code либо наблюдаемый ответ.

\n

Любой пропуск получает явную отметку: not run, not applicable с причиной или unknown. Пока не доказана связь между пакетом, образом и runtime-сценарием, предупреждение не закрыто. Так команда принимает не «самую новую версию», а изменение с понятной областью действия и обратным ходом.

\n

Проверяемые источники

" }