וואלה32 רקטות מוכנות לשיגור: צה"ל איתר בדרום לבנון - טרם הפסקת האשDaily MaverickWHAT’S COOKING: A trio of venison recipes from the heart of the KarooPunch2027: APC govs distance themselves from Wike-led rainbow coalition, deny allianceESPN59 points for the Bears? 41 for the Ravens? Let's size up four NFL offenses that erupted in Week 1Bollywood HungamaEXCLUSIVE: Himesh Reshammiya reunites with Vikram Bhatt after 13 years for 1920: Cold Winter; horror flick to be shot in MussoorieRTP DesportoI Liga. Braga Vence Estoril por 1-0 com Golo Decisivo de Jonas WindThe Jerusalem PostRussian frigate fires flares at NATO member Denmark's military helicopter in international watersInquirer Entertainment‘Forgotten Island’ introduces underrepresented Filipino culture to the worldUOLGoverno acredita que decisão firme do STF sobre Moraes reduz margem para intervenção dos EUANMEWatch Charli XCX bring out Kim Petras for ‘Unlock It’ in BrooklynIl Fatto QuotidianoIntossicazione dopo la festa dei soci di un istituto bancario: avviate le verifiche sul cateringNotJustOkFull list of winners at the 2026 AFRIMMA Awards
The Daily Newsstand · Free, Always
Tuesday, September 15, 2026

Сканер нашёл уязвимость в библиотеке, которую вы не подключали

Translate

В сборке сканер зависимостей начал ругаться на пакет, которого я не находил в проекте. Я открыл файл зависимостей, поискал по имени - нет его там. Поискал по всему репозиторию - нет. Импорта в коде тоже нет.

Первая версия была, что сканер смотрит не на тот проект. Вторая - что он тянет базу с ошибкой. Обе неверные. Пакет действительно был установлен, просто его туда никто не клал руками.

Один объявленный пакет, пять установленных

Проверяется это за одну команду. Файл зависимостей содержит ровно одну строку:

$ cat requirements.txt
requests==2.31.0
$ pip install -r requirements.txt
$ pip freeze
certifi==2026.7.22
charset-normalizer==3.5.1
idna==3.19
requests==2.31.0
urllib3==2.7.0

Объявлен один пакет, установлено пять. Четыре из них - зависимости requests: HTTP-транспорт, разбор кодировок, разбор международных имён, набор корневых сертификатов. Все они попадают в ваш процесс, исполняются в нём и обрабатывают те же данные.

Это не особенность Python. В экосистемах, где пакеты мельче - в первую очередь в npm, - дерево куда глубже: одна строка в манифесте разворачивается в сотни установленных пакетов, и это обычная, а не аварийная картина.

Ваш код - это всё дерево, а не его корень

Здесь и лежит расхождение между тем, как разработчик думает о проекте, и тем, что реально работает на сервере.

Разработчик думает списком: вот пакеты, которые я выбрал, прочитал про них, доверяю авторам. Работает - граф: выбранные пакеты плюс всё, что они привели с собой, плюс всё, что привели те.

Уязвимость в разборе кодировок, в клиенте HTTP, в парсере дат - это уязвимость в вашем приложении. Тот факт, что вы этот пакет не выбирали и его имени не знали, не меняет ни то, где он исполняется, ни то, к каким данным у него доступ.

Сканер, в отличие от вас, читает граф. Поэтому он и «находит то, чего нет».

Как это выглядит в реальном отчёте

Возьмём такой же проект, но со старым закреплённым urllib3 - типичная картина для окружения, которое не пересобирали год. Выгрузим состав окружения в файл и прогоним его через pip-audit: это официальный инструмент, который сверяет версии из такого файла с базой уязвимостей.

$ pip freeze > installed.txt
$ pip-audit -r installed.txt
Found 13 known vulnerabilities in 2 packages
Name     Version ID              Fix Versions
-------- ------- --------------- -------------
requests 2.31.0  PYSEC-2026-1873 2.32.0
requests 2.31.0  PYSEC-2026-1872 2.32.4
requests 2.31.0  PYSEC-2026-2275 2.33.0
urllib3  1.26.5  PYSEC-2023-192  1.26.17,2.0.6
urllib3  1.26.5  PYSEC-2023-192  1.26.17,2.0.6
urllib3  1.26.5  PYSEC-2023-212  1.26.18,2.0.7
urllib3  1.26.5  PYSEC-2023-212  1.26.18,2.0.7
urllib3  1.26.5  PYSEC-2026-141  2.7.0
urllib3  1.26.5  PYSEC-2026-1999 2.5.0
urllib3  1.26.5  PYSEC-2026-1998 2.6.0
urllib3  1.26.5  PYSEC-2026-1995 1.26.19,2.2.2
urllib3  1.26.5  PYSEC-2026-1994 2.6.0
urllib3  1.26.5  PYSEC-2026-1996 2.6.3

Вывод приведён целиком, включая две записи, которые продублировались, - они пришли из двух источников базы. Уникальных уязвимостей тут одиннадцать: три в requests и восемь в urllib3. При этом в файле зависимостей упомянут только requests, а бо́льшая часть находок - в urllib3, который туда попал сам.

Почему обновить прямую зависимость не всегда достаточно

Здесь я и застрял в первый раз. Логика была такая: urllib3 приехал вместе с requests, значит, обновлю requests - подтянется и новый urllib3.

Иногда так и происходит. Но условие в манифесте пакета выглядит как диапазон, а не как точная версия: «мне нужен urllib3 не ниже такой-то и ниже такой-то». Пока установленная версия попадает в диапазон, менеджер пакетов её не заменяет: условие уже выполнено. Обновление корня само по себе ничего не гарантирует.

Прямой способ - задать транзитивному пакету собственное требование. В Python это отдельный файл ограничений, в других экосистемах - секция переопределений в манифесте. Механизм есть везде, называется по-разному.

Порог берётся по максимальной из версий в колонке Fix Versions - среди строк того пакета, который вы чините, а не по первой попавшейся. Для urllib3 самая старшая там 2.7.0, и меньшее значение оставит часть находок открытыми:

$ cat constraints.txt
urllib3>=2.7.0
$ pip install -r requirements.txt -c constraints.txt

Здесь важно различать два случая, и они сильно разные по цене.

Если нужная версия попадает в диапазон, который объявила прямая зависимость, - а requests требует urllib3 <3,>=1.21.1, то есть 2.7.0 туда попадает, - вы просто подняли нижнюю границу внутри разрешённого. Это безопасно, и именно так работает файл ограничений.

Если же исправление вышло за верхнюю границу, которую объявил корневой пакет, - файлом ограничений вы уже не обойдётесь, нужен явный обход, и вместе с ним вы берёте на себя ответственность за совместимость. Границу объявили не просто так, и поломка вылезет в проде. Такой обход - временная мера до выхода версии корневого пакета с исправленным требованием, и у неё должен быть срок.

Почему без фиксации версий весь разговор бессмыслен

Всё вышесказанное имеет смысл, только если состав дерева воспроизводим. Если в файле зависимостей стоят диапазоны, то сборка в понедельник и сборка в пятницу дадут разные версии транзитивных пакетов, и результат вчерашней проверки к сегодняшней сборке отношения не имеет.

Отсюда простое требование: в сборку идёт зафиксированный полный список - lock-файл или его эквивалент, где перечислены все пакеты дерева с точными версиями. Проверяется этот же файл.

Следующий уровень - фиксация не версий, а содержимого: хеш каждого файла пакета. Тогда подмена уже опубликованной версии в репозитории пакетов ломает установку, а не проходит незаметно. В Python это режим --require-hashes, который надо включить явно. В npm и в современных менеджерах для Python на базе lock-файлов контрольные суммы пишутся в lock сами и проверяются при установке - там достаточно не выкидывать lock-файл из репозитория.

Ограничения

Список уязвимостей - это список известных уязвимостей, то есть тех, для которых кто-то оформил запись в базе. Чистый отчёт означает «ничего не заявлено», а не «ничего нет».

Большая часть находок в дереве неприменима: уязвимость в функции, которую ваш код не вызывает ни прямо, ни через библиотеку. Разбор этого стоит времени, и тратится оно каждую неделю. Инструменты, рассчитывающие достижимость кода, существуют, но ошибаются в обе стороны, и я бы не выключал по их вердикту ничего важного.

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

И SBOM - перечень состава сборки - сам по себе ничего не защищает. Его ценность в другом: когда завтра объявят уязвимость в пакете из глубины дерева, вопрос «а он у нас есть и где» должен решаться запросом к списку, а не обходом репозиториев руками.

Что посмотреть у себя

Сравнить число строк в файле зависимостей с числом реально установленных пакетов. Разница в разы - норма, и она же ответ на вопрос, почему сканер находит незнакомые имена.

Проверить, есть ли в сборке фиксация всего дерева, а не только прямых зависимостей. Если менеджер пакетов при повторной сборке может выбрать другую версию - воспроизводимости нет.

Прогнать pip-audit (или аналог для своей экосистемы) на зафиксированном составе окружения и посмотреть, сколько находок приходится на пакет, которого нет в манифесте. Обычно это большинство.

Найти, кто у вас смотрит эти отчёты и что делает по итогам. Отчёт, который формируется и никем не открывается, - самая частая находка в этой теме, и она не техническая.

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.