{ "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 и останавливает деплой.
Проверяемый вывод должен быть уже: какой пакет найден, кто его привёл, в каком артефакте он оказался, достигается ли уязвимая ветка в названном сценарии и можно ли вернуться к исходному digest. Предупреждение scanner — вход в расследование, а не доказательство ни безопасности, ни эксплуатации.
\nЗависимость может отсутствовать в корневом манифесте и всё равно входить в установленное дерево. Приложение зависит от http-client, тот — от parser, а advisory указывает на старую версию parser. Нужно проверить не только имя и версию, но и путь от production root до компонента.
Есть и обратная граница. Запись в lockfile описывает результат разрешения, но не доказывает, что пакет физически попал в конкретный образ. Сборка может исключить dev-зависимость, заменить optional-ветку, собрать другой workspace или использовать другой lockfile. Поэтому после дерева проверяют именно выходной артефакт и его digest.
\nРазделите объекты до первого изменения. Манифест описывает намерение проекта и диапазоны прямых зависимостей. Lockfile фиксирует разрешённое дерево. SBOM (Software Bill of Materials) перечисляет компоненты и отношения поставки для названного артефакта. Runtime evidence показывает наблюдение конкретного процесса и сценария. Каждый слой отвечает на свой вопрос и не заменяет соседний.
\nСначала зафиксируйте baseline: commit, package manager, Node.js, настройки .npmrc, команду установки и digest текущего артефакта. Затем назовите candidate: пакет, исходную и целевую версии, advisory и предполагаемый путь обновления. Без baseline нельзя отличить удаление уязвимого узла от его перемещения в другую ветку.
| Слой | Вопрос | Что сохранить |
|---|---|---|
package.json | Какие direct dependencies и диапазоны объявлены? | Commit и diff манифеста |
package-lock.json | Какое дерево разрешено зафиксированным проектом? | Полный diff, integrity и настройки resolver |
node_modules / image | Что физически установлено в проверенном артефакте? | Имя образа, digest и состав слоя |
| SBOM | Какие компоненты заявлены для named artifact? | Формат, источник генерации и связь с digest |
| Runtime | Что произошло в названном entry point? | Команду, вход, ответ, лог и отрицательный путь |
Ниже команды для проекта на npm. Подставьте в PACKAGE имя из advisory и выполняйте их в том workspace, который собирается в production. Если pipeline использует --legacy-peer-deps, workspaces или иной .npmrc, повторите те же настройки: разрешение зависит не только от текста манифеста.
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\nnpm ci — контрольная точка. По официальной документации ему нужен существующий lockfile; при расхождении с package.json команда завершается ошибкой, удаляет имеющийся node_modules и не переписывает манифест или lockfile. Если установка упала, это отдельный результат: сначала разберите peer-диапазоны, версию npm и флаги, с которыми lockfile был создан.
Не заменяйте npm ci на npm install ради зелёного exit code: обычная установка может пересчитать lockfile. Флаг --omit=dev убирает dev-зависимости с диска, но записи о них остаются разрешёнными в lockfile. Если build использует dev-инструменты, описывайте build-артефакт отдельной проверкой.
npm ls --all --omit=dev --json показывает логическое дерево установленных пакетов и может отметить missing, invalid или extraneous узлы. npm explain даёт обратный путь — почему пакет присутствует. Нулевой вывод не закрывает advisory, пока не проверены правильные workspace, lockfile, режим установки и artifact.
Одна строка parser@1.0.0 отвечает только на вопрос о версии. Один пакет может присутствовать несколько раз из-за несовместимых диапазонов. Логическое дерево npm также не равно физическому расположению на диске: deduplication и peer-зависимости меняют картину. Для решения нужны все пути от корня.
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 и что опасная ветка вызывается. Учитывайте это ограничение при формулировке результата.
SBOM полезен только вместе с идентичностью сборки: commit, digest, временем и понятным источником генерации. Файл с названием sbom.json без связи с release candidate может относиться к соседнему образу. Сверяйте компонент, версию, источник и relationship; расхождение с lockfile — сигнал расследования, а не повод выбрать более удобный результат.
Сканируйте immutable digest именно публикуемого образа или архива. Локальный node_modules, staging-образ и предыдущий digest не являются заменой. Если сборка создаёт несколько image layers, выясните, где лежит компонент и не остаётся ли старая копия в другом слое.
| Симптом | Гипотеза | Проверка | Действие |
|---|---|---|---|
Пакета нет в package.json | Он транзитивный или пришёл из другого workspace | npm ls --all и npm explain в production root | Назвать родителя и проверить каждый путь |
| Lockfile изменился, advisory остался | Осталась другая копия или ветка | Сравнить все записи и версии в дереве и SBOM | Обновить каждого владельца либо обосновать исключение |
npm ci зелёный, сервис не стартует | Не совпали Node.js, peer range, ABI или module format | Smoke в том же image и runtime | Остановить выпуск и сузить candidate |
| Сканер не видит старую версию | Проверен другой digest или слой | Сопоставить digest отчёта и release | Пересканировать публикуемый artifact |
| Rollback вернул версию, но данные не читаются | Изменился формат или внешний протокол | Проверить обратную совместимость миграции | Разделить security update и изменение данных |
Наличие пакета и достижимость уязвимого кода — разные утверждения. Назовите entry point, импорт или вызов, входные данные и режим исполнения. Для сервера это HTTP-обработчик, для CLI — команда, для worker — сообщение из очереди. Формулировка «ветка не достигнута в проверенном сценарии» честнее, чем «уязвимости нет».
\nПроверьте optional-зависимости на целевой платформе, peer-зависимости, bundled packages, worker, cron и отдельный CLI. Сборщик может исключить статический импорт, а динамический require сохранить путь. Строковый поиск находит имя, но не учитывает условие, экспорт, bundler и конфигурацию.
Минимальное evidence — связка dependency path, содержимого production-артефакта и названного сценария с ожидаемым результатом. Добавьте отрицательный путь: некорректный вход должен быть отклонён ожидаемым способом, а недоступная optional-часть не должна молча создавать видимость исправности.
\nСформируйте небольшой candidate. Обновляйте прямого родителя, если он выпускает совместимую версию, или добавляйте явное разрешение с объяснением владельца риска. Не меняйте одновременно Node.js, package manager и несколько крупных библиотек: rollback перестанет показывать причину отказа.
\nПрочитайте весь diff lockfile: resolved URL, integrity, peer и optional-поля, количество копий и новые install scripts. Поле engines выражает заявленный диапазон совместимости. Без engine-strict npm может оставить предупреждение и продолжить, а строгая проверка всё равно не запускает приложение и не проверяет native addon.
Повторите clean install с параметрами pipeline. Затем запустите startup, импорт компонента, валидный и невалидный вход, сборку, сетевой вызов и затронутый CLI. У каждого теста должны быть команда, окружение и exit code. Фраза «всё прошло» без этого набора не является доказательством.
\n.npmrc и digest текущего артефакта.npm ci, затем сохраните npm ls --all --omit=dev --json и npm explain PACKAGE. Разберите каждый путь и копию.Зелёный npm audit не означает, что внешний scanner ошибся: базы advisory, области установки и правила инструментов различаются. Красный scanner тоже не доказывает достижимость вызова. Зафиксируйте источник, версию базы, путь пакета и artifact, затем согласуйте решение с владельцем риска.
Установка с --ignore-scripts не проверяет install script, который нужен приложению. macOS-проверка не заменяет Linux-образ, если native dependency собирается в CI. Локальная папка не заменяет image digest. Эти ограничения пишутся рядом с результатом, иначе отчёт создаёт ложную уверенность.
Маршрут рассчитан на npm-проекты с package-lock.json. Для Yarn, pnpm, Cargo, Maven или системных пакетов команды и формат lockfile будут другими, хотя разделение manifest, resolved tree, artifact и runtime остаётся полезной моделью. Не переносите npm-команды в другой менеджер без сверки его документации.
Статья не определяет exploitability и не заменяет threat model, code review или расследование инцидента. Достижимость зависит от кода, конфигурации, прав, входных данных и внешних сервисов. Если не удалось получить точный digest, выполнить сценарий или сопоставить SBOM с build, статус должен быть unknown, а не safe.
Major-обновление требует отдельной оценки API-изменений. Для native addon добавьте проверку ABI и целевой платформы. Для private registry сохраняйте provenance, но не публикуйте токены и приватные URL в отчёте.
\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-сценарием, предупреждение не закрыто. Так команда принимает не «самую новую версию», а изменение с понятной областью действия и обратным ходом.
package.json, очистке node_modules и неизменности lockfile во время CI-установки.--all, --json и ограничение: вывод не равен физическому расположению на диске.engines и его advisory-характер без engine-strict.