Безопасность зависимостей и supply chain
Разбираем, почему чужой код в зависимостях — это ваша зона ответственности, и как защитить цепочку поставок: lock-файлы, проверка целостности, контроль обновлений.
Атака на цепочку поставок (supply chain) — компрометация приложения не напрямую, а через одну из его зависимостей: библиотеку, пакет или инструмент сборки, которому оно доверяет.
Вы пишете малую часть кода, который реально выполняется у пользователя. Остальное — фреймворки, утилиты, транзитивные зависимости зависимостей. Категория Vulnerable and Outdated Components входит в OWASP Top 10 именно потому, что уязвимость в популярной библиотеке мгновенно становится уязвимостью тысяч приложений. Этот урок — про принципы защиты цепочки поставок, без рецептов атаки на чужие реестры.
Зачем это знать разработчику
Самый громкий пример риска — уязвимость Log4Shell в библиотеке логирования Log4j (2021): одна строка в зависимости открывала удалённое выполнение кода в огромном числе Java-приложений по всему миру. Разработчики этих приложений не писали уязвимый код — они просто подключили популярную библиотеку. Вывод защитника: доверие к зависимости — это принятый риск, которым нужно управлять, а не игнорировать.
Откуда берётся риск
Известные уязвимости (CVE) в версиях
В библиотеке находят уязвимость, ей присваивают идентификатор CVE и публикуют исправление в новой версии. Но ваше приложение продолжает тянуть старую версию из lock-файла — и остаётся уязвимым, пока вы не обновитесь. Чем дольше зависимость не трогают, тем больше накапливается известных дыр. Поэтому «работает — не трогай» в безопасности не действует: устаревшая зависимость опаснее обновлённой.
Транзитивные зависимости
Вы подключаете пакет A, он тянет B, B тянет C. Уязвимость может оказаться в C — пакете, о котором вы даже не слышали. В графе зависимостей крупного проекта легко тысячи узлов. Поэтому проверять нужно весь граф, а не только прямые зависимости из вашего манифеста.
Компрометация самого пакета
Опаснее устаревания — когда вредоносный код попадает в зависимость намеренно. Типичные сценарии (понимать, чтобы защищаться): типосквоттинг — пакет с именем, похожим на популярный (опечатка в одну букву), в надежде, что кто-то ошибётся при установке; захват заброшенного пакета через угнанный аккаунт мейнтейнера; внедрение бэкдора в новую версию. Защита строится на проверке целостности и контроле, что именно вы устанавливаете.
Жизненный цикл уязвимости: от находки до фикса
Полезно понимать, как уязвимость в библиотеке проходит свой путь — это объясняет, почему важна скорость реакции. Сначала исследователь находит проблему и ответственно сообщает её мейнтейнеру (responsible disclosure), не публикуя детали сразу. Мейнтейнер выпускает исправленную версию. Затем уязвимости присваивают идентификатор CVE (Common Vulnerabilities and Exposures) и публикуют в общедоступных базах. С этого момента о дыре знают все, в том числе злоумышленники, — и начинается гонка: кто быстрее, защитники с обновлением или атакующие со сканированием уязвимых версий в интернете. Поэтому промежуток между публикацией CVE и вашим обновлением — это и есть окно риска, и его нужно сокращать.
Каждой уязвимости присваивают оценку серьёзности по шкале CVSS (Common Vulnerability Scoring System) — число от 0 до 10, которое учитывает, насколько легко эксплуатировать дыру и каков потенциальный ущерб. Условно: 9.0–10.0 — критическая, 7.0–8.9 — высокая, 4.0–6.9 — средняя, ниже — низкая. Эта оценка помогает приоритизировать: критические CVE в используемых зависимостях чинят немедленно, низкие — в плановом порядке. SCA-сканеры показывают и CVE, и его CVSS-балл, чтобы вы не тратили силы на разбор каждой находки одинаково.
Как это работает под капотом
Менеджер пакетов при установке читает манифест (package.json, requirements.txt), разрешает диапазоны версий в конкретные версии и скачивает их из реестра. Ключевой момент — разрешение версий. Если в манифесте указан диапазон (например, «любая 4.x»), то при разных установках может подтянуться разная версия. Это и есть точка риска: сборка становится недетерминированной, а в неё может незаметно попасть новая, уже скомпрометированная версия. Решение — зафиксировать точные версии и проверять, что скачанный файл не подменили.
Как защититься
1. Lock-файлы — фиксация точных версий. Lock-файл (package-lock.json, poetry.lock, Cargo.lock) записывает точную версию каждой зависимости, включая транзитивные. Сборка становится воспроизводимой: на машине разработчика, в CI и в проде ставится один и тот же код. Lock-файл обязательно коммитят в репозиторий.
{
"name": "lodash",
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvKw=="
}
2. Проверка целостности (integrity). Обратите внимание на поле integrity выше — это криптографический хеш содержимого пакета. При установке менеджер сверяет хеш скачанного файла с записанным в lock-файле. Если в реестре подменили содержимое версии — хеши не совпадут, и установка прервётся. Это защита от подмены «на лету».
3. Детерминированная установка в CI. В конвейере используйте режим установки строго по lock-файлу, без обновления версий:
# Ставит ровно версии из lock-файла, падает при расхождении
npm ci # вместо npm install
pip install --require-hashes -r requirements.txt
4. Регулярные обновления и SCA-сканирование. Парадокс безопасности: устаревшие зависимости опаснее свежих. Настройте автоматическое сканирование (как в прошлом уроке — npm audit, pip-audit, Trivy) и бота обновлений (Dependabot/Renovate), который открывает PR с апдейтами. Обновление — это снижение риска, а не «лишняя суета».
5. Минимизируйте поверхность. Каждая зависимость — это доверие к её авторам и всему её графу. Не тяните пакет ради одной короткой функции, которую можно написать самому. Меньше зависимостей — меньше точек компрометации.
6. SBOM — опись состава. Software Bill of Materials — это полный список того, из чего собрано приложение (все пакеты и версии). Когда выходит новая критичная CVE, по SBOM за минуты понятно, затронуты вы или нет, без ручного перебора. Многие сканеры генерируют SBOM автоматически.
Итоги
- Большая часть исполняемого кода — это зависимости; уязвимость в них становится вашей уязвимостью.
- Риск идёт от устаревших версий (известные CVE), транзитивных зависимостей и прямой компрометации пакета (типосквоттинг, угон мейнтейнера).
- Lock-файлы фиксируют точные версии всего графа, а поле
integrityзащищает от подмены содержимого. - В CI ставьте зависимости строго по lock-файлу (
npm ci), регулярно обновляйтесь и сканируйте через SCA. - Минимизируйте число зависимостей и держите SBOM, чтобы быстро реагировать на новые CVE.