Уязвимости в зависимостях
Большая часть кода в вашем релизе написана не вами — и именно там чаще всего находят уязвимости.
Уязвимость в зависимости — это известный дефект безопасности (обычно с номером 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) и дисциплина регулярных апдейтов превращают защиту из аврала в рутину.