Безопасность зависимостей и 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.
Проверьте себя
1. Зачем коммитить lock-файл (package-lock.json, poetry.lock) в репозиторий?
AЧтобы зафиксировать точные версии всех зависимостей (включая транзитивные) и сделать сборку воспроизводимой и предсказуемой
BЧтобы ускорить выполнение кода в продакшене
CЧтобы скрыть список используемых библиотек от посторонних
DLock-файл вообще не нужен и его добавляют в .gitignore
2. Что такое типосквоттинг в контексте безопасности зависимостей?
AВредоносный пакет с именем, похожим на популярный (отличие в одну букву), в расчёте на опечатку при установке
BОпечатка в имени переменной внутри вашего кода
CСпособ ускорить установку пакетов через кеш
DФормат записи версий в lock-файле