Цепочка поставок: зависимости и lock-файлы

Большая часть вашего приложения — это код, который написали не вы.

Цепочка поставок ПО (software supply chain) — все внешние компоненты, на которых держится приложение: библиотеки, их зависимости, инструменты сборки. SCA — анализ состава ПО на известные уязвимости.

Почему зависимости — это риск

Современный сервис тянет сотни транзитивных зависимостей. Каждая — чужой код, исполняющийся с вашими правами. Уязвимость в популярной библиотеке автоматически становится вашей уязвимостью, даже если ваш собственный код безупречен. Несколько громких инцидентов последних лет — именно из-за дыр в широко используемых пакетах.

Стоит осознать масштаб несоответствия. Команда тщательно ревьюит каждую строчку своего кода, но этот код — нередко лишь несколько процентов от того, что в итоге исполняется в продакшене. Остальное — пакеты, которые никто из команды построчно не читал, и пакеты этих пакетов, о существовании которых вы можете и не знать. Получается, что львиная доля вашей реальной поверхности атаки находится в коде, который вы не писали и не проверяли. Это не повод от него отказываться — переписывать всё самому и дороже, и опаснее, — но повод относиться к зависимостям как к полноценной части системы, а не как к «просто библиотекам».

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

Известные уязвимости и SCA

Большинство проблем с зависимостями — не «новый взлом», а уже известные уязвимости (CVE), для которых давно есть исправление, но проект не обновился. SCA-инструменты (audit, Dependabot, OWASP Dependency-Check, Snyk) сверяют ваш список зависимостей с базами уязвимостей и сообщают, что и до какой версии обновить.

# Базовая проверка зависимостей на известные уязвимости
npm audit            # Node
pip-audit            # Python
# в CI: блокировать сборку при уязвимостях высокой критичности

Ключ — встроить SCA в CI и реагировать регулярно, а не раз в год: окно между публикацией уязвимости и её эксплуатацией коротко. Публикация CVE — это сигнал не только для защитников, но и для атакующих: как только дефект и способ его эксплуатации становятся общеизвестны, по интернету начинают массово сканировать уязвимые версии. Поэтому ценность измеряется не фактом «у нас есть сканер», а скоростью реакции: насколько быстро после появления исправления оно доезжает до прода.

Чтобы SCA приносил пользу, а не превращался в фоновый шум, важны две вещи. Во-первых, результат должен влиять на сборку: критичные уязвимости разумно делать блокирующими, иначе предупреждения копятся и их перестают замечать. Во-вторых, нужен механизм приоритизации и осознанного откладывания: не каждая найденная уязвимость реально достижима в вашем сценарии использования, и команде нужен внятный процесс, чтобы отличать «чинить сейчас» от «принять риск и зафиксировать решение», а не молча игнорировать весь отчёт.

Lock-файлы: воспроизводимость и целостность

Lock-файл (package-lock.json, poetry.lock и т.п.) фиксирует точные версии всех зависимостей, включая транзитивные, вместе с их контрольными суммами. Это даёт два эффекта безопасности: воспроизводимость (у всех и в проде ставится ровно то, что проверено) и целостность (хеш не даст незаметно подменить содержимое пакета).

// Уязвимо: плавающие версии — при каждой установке прилетает «что-то новое»
"dependencies": { "lib": "^1.0.0" }   // подтянет 1.x, включая свежее непроверенное

// Безопаснее: lock-файл фиксирует точные версии + хеши,
// установка в CI/проде идёт строго по нему (например, npm ci)

Риски supply chain и минимизация

  • Typosquatting. Пакет с именем, похожим на популярный, — проверяйте, что устанавливаете.
  • Захват пакета. Скомпрометированный мейнтейнер выпускает вредоносную версию — отсюда ценность lock-файла и осторожных обновлений.
  • Лишние зависимости. Каждая библиотека — поверхность атаки; не тяните пакет ради одной функции.

Минимизация поверхности применима и здесь: меньше зависимостей — меньше риска. Перед добавлением пакета оцените, активен ли он, много ли тянет за собой и нельзя ли обойтись стандартной библиотекой.

Как работает под капотом: дерево зависимостей и pinning

Менеджер пакетов строит дерево: ваши прямые зависимости тянут свои, те — свои. Реальная уязвимость часто сидит в транзитивной зависимости, о которой вы не знали. Lock-файл «прибивает» (pin) всё дерево к конкретным версиям и хешам; обновление становится осознанным шагом с возможностью прогнать тесты и SCA, а не случайностью при очередной установке.

Особенно поучительна роль хешей в lock-файле. Зафиксировать номер версии мало: в редких, но крайне опасных случаях содержимое уже опубликованного пакета может оказаться подменено. Контрольная сумма закрывает это: при установке менеджер сверяет хеш скачанного пакета с записанным в lock-файле, и при несовпадении установка падает, а не тихо подтягивает изменённый код. Так lock-файл из инструмента «чтобы у всех совпадали версии» превращается ещё и в контроль целостности цепочки поставки.

Прямые зависимости (вы их выбрали)
   |-- lib-a 1.4.2
   |       |-- util-x 0.9.1   <- транзитивная, о ней вы не думали
   |       \-- util-y 2.0.0
   \-- lib-b 3.1.0
           \-- util-x 0.9.1   (та же, переиспользована)
Lock-файл фиксирует ВСЁ дерево: версии + хеши.

Здесь же лежит и ответ на типичный вопрос «фиксировать версии или всегда брать свежее?». Свежее автоматически звучит безопаснее, но «плавающие» диапазоны означают, что в любой момент в прод может приехать версия, которую вы не тестировали и не сканировали, — в том числе сразу после компрометации пакета. Зафиксированное дерево плюс дисциплина регулярных, но осознанных обновлений даёт лучшее из двух миров: вы не отстаёте от исправлений, но каждое обновление проходит через тесты и SCA, а не просачивается незаметно.

Частые ошибки

  • Не обновлять зависимости. Большинство дыр — известные и уже исправленные выше.
  • Игнорировать lock-файл / не коммитить его. Теряются воспроизводимость и контроль целостности.
  • Тянуть лишние пакеты. Раздувают поверхность атаки.
  • SCA «когда-нибудь». Проверки нужны в CI и регулярно.

Итоги

  • Зависимости — чужой код с вашими правами и часть поверхности атаки.
  • Большинство уязвимостей зависимостей известны: используйте SCA в CI и обновляйтесь.
  • Lock-файлы дают воспроизводимость и целостность; коммитьте и уважайте их.
  • Минимизируйте число зависимостей и проверяйте, что устанавливаете.
Проверьте себя
1. Почему сторонние зависимости — это часть поверхности атаки?
AОни всегда написаны плохо
BЭто чужой код, исполняющийся с вашими правами; его уязвимость становится вашей, даже если ваш код безупречен
CОни замедляют сборку
DОни занимают место на диске
2. Что обеспечивает lock-файл с точки зрения безопасности?
AШифрование исходного кода
BВоспроизводимость (везде ставится одно и то же) и целостность (хеши не дают незаметно подменить содержимое пакета)
CУскорение установки в десятки раз
DАвтоматическое исправление уязвимостей
3. Почему большинство уязвимостей в зависимостях — это известные проблемы?
AПотому что их невозможно исправить
BЭто опубликованные CVE с уже выпущенными исправлениями, но проект не обновился; SCA и обновления их закрывают
CПотому что библиотеки не тестируют
DПотому что они появляются только в проде