Уязвимости в зависимостях

Большая часть кода в вашем релизе написана не вами — и именно там чаще всего находят уязвимости.

Уязвимость в зависимости — это известный дефект безопасности (обычно с номером CVE) в сторонней библиотеке, который наследует ваше приложение просто потому, что вы её подключили.

Когда вы пишете import requests или добавляете строку в package.json, вы берёте на себя ответственность не только за этот пакет, но и за всё, что он тянет за собой. Один прямой пакет может потянуть десятки транзитивных. Если в любом из них есть уязвимость, она оказывается в вашем процессе с вашими правами и доступом к вашим данным.

Зачем это знать защитнику

Громкие инциденты последних лет — Log4Shell в библиотеке Log4j, уязвимости десериализации, дыры в парсерах — это не атаки на чей-то самописный код, а эксплуатация популярных зависимостей. Защитнику важно понимать: ваша поверхность атаки — это не только ваш репозиторий, но и весь граф зависимостей. Управление этим риском называется SCA (Software Composition Analysis) — анализ состава ПО.

Откуда берётся CVE и как он до вас доходит

CVE (Common Vulnerabilities and Exposures) — это публичный идентификатор конкретной уязвимости, например CVE-2021-44228. Когда исследователь находит дыру, ей присваивают номер, описание и оценку серьёзности по шкале CVSS (от 0 до 10). Эти записи попадают в базы: NVD, GitHub Advisory Database, OSV. Сканеры сравнивают версии ваших пакетов с этими базами.

Путь уязвимости к вам обычно такой: пакет A, который вы подключили явно, зависит от B, а B — от уязвимого C. Вы про C можете даже не знать. Поэтому смотреть нужно на полный граф, а не на список прямых зависимостей.

Lock-файлы: фиксация точного состава

Манифест (package.json, requirements.txt с диапазонами, pom.xml) описывает желаемые версии, часто диапазонами вроде ^4.17.0. А lock-файл фиксирует точные установленные версии всего графа, включая транзитивные, вместе с хешами.

{
  "name": "lodash",
  "version": "4.17.21",
  "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
  "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvKw=="
}

Поле integrity — это криптографический хеш содержимого пакета. При установке менеджер пересчитывает хеш скачанного архива и сравнивает: если кто-то подменил содержимое в реестре, установка упадёт. Lock-файл даёт три вещи: воспроизводимость (у всех ставится одно и то же), защиту целостности (через хеши) и прозрачность (виден весь реальный состав).

Как обнаружить уязвимые зависимости

Базовый защитный приём — регулярно прогонять аудит зависимостей. Большинство экосистем имеют встроенный сканер:

# Node.js — встроенный аудит
npm audit
npm audit --audit-level=high   # показывать только high/critical

# Python
pip-audit

# Кросс-языковой сканер на основе базы OSV
osv-scanner --lockfile=package-lock.json

Типичный отчёт показывает пакет, найденный CVE, серьёзность и — что важно — есть ли исправленная версия:

High   Prototype Pollution in lodash
  Package      lodash
  Vulnerable   < 4.17.21
  Patched in   >= 4.17.21
  Path         myapp > some-lib > lodash
  Advisory     https://github.com/advisories/GHSA-...

Строка Path бесценна: она показывает, через какую цепочку пришла уязвимость. Если это транзитивная зависимость, обновлять нужно тот пакет, который её тянет, либо использовать механизм переопределения версий (overrides в npm, resolutions в Yarn).

Как это работает под капотом

Сканер не «анализирует» код пакета на наличие дыр в реальном времени. Он делает сопоставление: читает lock-файл, получает список пар «имя + версия», нормализует их в идентификаторы вида pkg:npm/[email protected] (формат PURL, package URL) и спрашивает у базы уязвимостей, попадает ли версия в уязвимый диапазон. Это сравнение, а не статический анализ, поэтому оно быстрое, но зависит от полноты и свежести базы.

Отсюда два следствия. Во-первых, сканер видит только известные и опубликованные уязвимости — нулевой день он не поймает. Во-вторых, возможны ложные срабатывания: уязвимый код может быть в пакете, но в недостижимой для вас функции. Поэтому серьёзные инструменты добавляют анализ достижимости (reachability), но базовую гигиену даёт даже простое сопоставление версий.

Как защититься

Защита от уязвимых зависимостей — это процесс, а не разовое действие:

  • Всегда коммитьте lock-файл в репозиторий и устанавливайте из него (npm ci, а не npm install в CI). Это гарантирует воспроизводимость и проверку хешей.
  • Встройте SCA в CI. Пусть пайплайн падает на high/critical уязвимостях. Уязвимость, найденная при сборке, дешевле, чем найденная в проде.
  • Автоматизируйте обновления. Боты вроде Dependabot или Renovate открывают pull request с обновлением уязвимого пакета. Ваша задача — прогнать тесты и смержить, а не вручную отслеживать сотни пакетов.
  • Обновляйтесь регулярно, а не «когда рванёт». Проект, отставший на мажорные версии, не сможет быстро накатить патч безопасности, потому что патч выходит только для свежих веток. Технический долг по зависимостям — это отложенный риск.
  • Минимизируйте граф. Каждая лишняя зависимость — это лишний риск. Перед добавлением пакета ради одной функции подумайте, нельзя ли написать её самому.

Итоги

  • Большая часть кода в релизе — чужая, и уязвимости (CVE) чаще всего приходят именно из зависимостей, в том числе транзитивных.
  • Lock-файл фиксирует точные версии всего графа и хеши, давая воспроизводимость и защиту целостности.
  • SCA-сканеры (npm audit, pip-audit, osv-scanner) сопоставляют версии с базами уязвимостей — встройте их в CI.
  • Автообновления (Dependabot/Renovate) и дисциплина регулярных апдейтов превращают защиту из аврала в рутину.
Проверьте себя
1. Зачем коммитить lock-файл в репозиторий, если есть манифест с версиями?
ALock-файл фиксирует точные версии всего графа зависимостей и их хеши, обеспечивая воспроизводимость и проверку целостности
BLock-файл нужен только для ускорения установки и на безопасность не влияет
CМанифест содержит секреты, поэтому его не коммитят, а lock-файл — публичный
DLock-файл автоматически исправляет уязвимости при установке
2. Что в первую очередь делает SCA-сканер вроде osv-scanner?
AЗапускает код каждого пакета в песочнице и ищет вредоносное поведение
BСопоставляет имена и версии пакетов из lock-файла с базами известных уязвимостей
CПереписывает уязвимый код пакетов на безопасный
DПроверяет цифровые подписи всех разработчиков пакетов