Сканер нашёл уязвимость в библиотеке, которую вы не подключали
Во время очередной сборки сканер зависимостей сообщил о проблеме в пакете, которого не оказалось ни в файле зависимостей, ни в исходном коде. Поиск по репозиторию также ничего не дал: название библиотеки нигде не упоминалось, импорт отсутствовал.
Сначала возникло предположение, что инструмент проверяет не тот проект или использует ошибочную базу данных уязвимостей. Однако проблема оказалась реальной: пакет действительно был установлен и выполнялся внутри приложения, просто попал туда не напрямую.
В проекте есть не только указанные библиотеки
Файл зависимостей может содержать всего одну строку:
```text
requests
```
Но после установки окружение будет включать не один пакет. Вместе с `requests` подтянутся библиотеки для HTTP-транспорта, обработки кодировок, разбора международных доменных имён и набор корневых сертификатов. В итоге объявлена одна зависимость, а фактически установлено несколько.
Проверить реальный состав окружения можно командой:
```bash
pip list
```
или экспортировать его в отдельный файл:
```bash
pip freeze > requirements.lock
```
Именно этот список, а не исходный манифест, показывает, какие компоненты действительно попадут в сборку.
Такие библиотеки называются транзитивными зависимостями. Они устанавливаются автоматически, потому что нужны пакетам верхнего уровня. В экосистеме Python дерево обычно остаётся относительно компактным. В npm одна строка в `package.json` нередко приводит к установке сотен дополнительных модулей.
Для разработчика проект часто выглядит как перечень выбранных им библиотек. Для операционной системы и сканера это граф: прямые зависимости, их зависимости и следующий уровень вложенности.
Если уязвимость обнаружена в парсере, HTTP-клиенте или библиотеке обработки дат, она всё равно относится к вашему приложению. Пакет может быть выбран не вами, но он выполняется в том же процессе и потенциально работает с теми же входными данными.
Почему сканер сообщает о библиотеке, которой нет в коде
Сканер анализирует не импорты и не только содержимое манифеста. Он проверяет фактически установленное дерево пакетов. Поэтому он видит компоненты, которые разработчик никогда не добавлял вручную.
Например, окружение с устаревшей версией `urllib3` может появиться после установки `requests`. В исходном файле будет указан только HTTP-клиент, но в отчёте уязвимость обнаружится в его внутренней зависимости.
Проверить такой набор можно через аудит экспортированного окружения:
```bash
pip-audit -r requirements.lock
```
В реальном отчёте отдельные записи иногда дублируются, если сведения пришли из нескольких источников. Поэтому важно считать не строки, а уникальные уязвимости.
В одном из подобных случаев было найдено одиннадцать проблем: три относились к `requests`, а восемь - к `urllib3`. При этом в манифесте присутствовал только `requests`, тогда как большая часть предупреждений касалась автоматически установленного пакета.
Почему обновление прямой зависимости может не помочь
Распространённая логика выглядит так: если `urllib3` поставляется вместе с `requests`, достаточно обновить `requests`, и вложенный пакет тоже станет свежим.
Иногда это действительно работает, но гарантии нет. В метаданных пакета обычно указывается не конкретная версия, а допустимый диапазон. Например:
```text
urllib3 >= 1.21.1, < 3
```
Если текущая версия соответствует этому условию, менеджер пакетов считает зависимость удовлетворённой и не обязан заменять её на последнюю доступную.
Получается парадоксальная ситуация: корневой пакет обновлён, а уязвимая транзитивная библиотека осталась прежней. Для менеджера зависимостей всё корректно - формальное требование выполнено.
Как зафиксировать безопасную версию
Если исправленная версия входит в разрешённый диапазон, транзитивную зависимость можно закрепить отдельно. В Python для этого часто используют файл ограничений:
```text
urllib3==2.7.0
```
Затем сборка выполняется с учётом ограничений:
```bash
pip install -r requirements.txt -c constraints.txt
```
В других инструментах аналогичная возможность может называться overrides, resolutions или dependency constraints. Название отличается, но принцип одинаков: разработчик задаёт конкретную версию вложенного пакета, не меняя основной манифест.
Выбирать версию следует по самой высокой отметке в поле исправлений для конкретной библиотеки. Нельзя ориентироваться на первую строку отчёта: одна и та же зависимость может иметь несколько уязвимостей, исправленных в разных релизах.
Если для `urllib3` максимальная исправленная версия указана как `2.7.0`, установка более раннего релиза не закроет все найденные проблемы.
Когда ограничение безопасно, а когда рискованно
Есть два принципиально разных сценария.
Если выбранная версия находится внутри диапазона, заявленного прямой зависимостью, изменение обычно безопасно. Например, `urllib3` версии `2.7.0` соответствует условию `>=1.21.1, <3`. В этом случае меняется только нижняя граница фактически устанавливаемого пакета. Если же исправление требует версии выше верхнего ограничения, ситуация становится сложнее. Принудительная установка может привести к несовместимости с корневой библиотекой: измениться API, поведение обработки ошибок или формат передаваемых данных. Такое переопределение допустимо как временный обход, но ответственность за совместимость ложится на команду проекта. Его следует сопровождать тестами, отдельной задачей на обновление основной зависимости и сроком удаления временного решения.
Без фиксации версий отчёт быстро устаревает
Аудит имеет смысл только при воспроизводимой сборке. Если в файле указаны широкие диапазоны, окружение, собранное в понедельник, может отличаться от окружения, созданного в пятницу.
Сегодня сканер проверит один набор версий, а завтра менеджер пакетов установит другой. В таком случае результат проверки не описывает конкретный артефакт, который попадёт в эксплуатацию.
Поэтому в сборку должен попадать полный зафиксированный список зависимостей: lock-файл или его аналог. В нём указываются все пакеты дерева и их точные версии, а нередко также контрольные суммы архивов.
Что стоит проверить в своём проекте
Начать аудит можно с нескольких практических шагов:
1. Сформировать полный список установленных пакетов.
2. Найти дерево зависимостей, а не только прямые записи.
3. Запустить сканер по lock-файлу или экспортированному окружению.
4. Отделить дубли в отчёте от уникальных уязвимостей.
5. Проверить диапазоны версий прямых зависимостей.
6. Закрепить безопасные транзитивные пакеты ограничениями.
7. Пересобрать окружение с нуля и повторить проверку.
8. Добавить аудит в CI/CD.
Важно проверять не только среду разработки, но и финальный контейнер или пакет, который разворачивается на сервере. В production иногда присутствуют дополнительные системные компоненты, необязательные зависимости и инструменты сборки, отсутствующие на локальной машине.
Почему одного сканирования недостаточно
Сканер показывает известные проблемы, но не гарантирует абсолютную безопасность. Уязвимость может ещё не быть опубликована в используемой базе, а опасное поведение способно возникнуть из-за неправильной конфигурации.
Кроме того, не каждая найденная проблема одинаково опасна. Нужно учитывать, используется ли уязвимый код, доступен ли атакующему соответствующий вход, работает ли библиотека в серверном или локальном сценарии и какие права имеет процесс.
Тем не менее игнорировать транзитивные зависимости нельзя. Минимальная зрелая практика включает фиксированный состав окружения, регулярное обновление, автоматическую проверку и понятную процедуру обработки исключений.
Главный вывод прост: приложение состоит не только из пакетов, которые разработчик указал вручную. Всё дерево зависимостей является частью поставляемого продукта. Поэтому контролировать нужно не корень манифеста, а весь граф библиотек, реально выполняющийся в вашем процессе.
