Должны ли библиотеки запрещать уязвимые версии зависимостей?

от автора

Когда в зависимости обнаруживают уязвимость, очевидное решение — поднять её минимально допустимую версию во всех библиотеках, которые ее используют. Тогда пакетный менеджер не подтянет уязвимую версию даже при новой установке. Сет Ларсон из Python Software Foundation предлагает не делать этого автоматически. По его мнению метаданные библиотеки должны описывать только совместимость, а контроль безопасности сборки оставаться на стороне приложения.

Колонка Сета вызвала спор в Python-сообществе: одни поддержали разделение ответственности между библиотекой и приложением, другие заметили, что безопасность тоже может входить в условия поддержки библиотеки. Мы в CodeScoring подготовили перевод колонки, разбор обеих позиций и наш взгляд на этот вопрос.

Диапазоны версий зависимостей в библиотеках не предназначены для исправления уязвимостей

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

[project]
name = «example-library»
dependencies = [
  «urllib3>=2»,
]

Прим. ред. Запись urllib3>=2 разрешает любую версию не ниже 2.0.0, включая будущие версии 3.x. Она задает нижнюю границу, но не ограничивает установку только веткой 2.x.

Теперь предположим, что в urllib3 обнаружена уязвимость, которая затрагивает версии до 2.6.2 включительно, а исправление выходит в версии 2.6.3. Позднее вы получаете от обеспокоенного пользователя пул-реквест, который поднимает минимальную версию с 2 до 2.6.3, чтобы «запретить установку уязвимой версии urllib3»:

[project]
name = «example-library»
dependencies = [
—  «urllib3>=2»,
+  «urllib3>=2.6.3»,
]

Скорее всего, такой пул-реквест принимать не стоит. Диапазоны версий в библиотеках нужны для описания совместимости, а не для устранения уязвимостей. В этом заключается одно из важных отличий библиотеки от приложения. Библиотекам следует допускать максимально широкий диапазон совместимых версий, а приложениям – фиксировать конкретные версии зависимостей с помощью lock-файла: requirements.txt с параметром —hash, pylock.toml или uv.lock.

Мейнтейнер библиотеки не обязан принуждать пользователей к безопасным версиям зависимостей, которыми библиотека не управляет напрямую, например, не включает их в собственную поставку. Это ответственность пользователей.

Почему так делать не стоит?

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

Более 10 000 других библиотек в Python Package Index указывают urllib3 как прямую зависимость. При таком подходе одна уязвимость потребовала бы нового выпуска всех 10 000 проектов. Для numpy, от которой зависят 80 000 библиотек, requests с 72 000 зависимых библиотек и pandas с 55 000 последствия были бы еще серьезнее.

Уязвимости в открытых библиотеках публикуются каждый день. Значит, новые релизы тоже пришлось бы готовить каждый день и продолжать это бесконечно. Ничего хорошего из этого не получится.

Гораздо эффективнее позволить пользователям самостоятельно управлять зависимостями своих приложений и проверять, не затрагивают ли их известные уязвимости.

Когда это все-таки может быть оправданно?

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

Возможен и другой сценарий. Ограничения вашей библиотеки не позволяют перейти на версию с исправлением: например, безопасная версия доступна только в ветке urllib3 2.x, а ваша библиотека совместима лишь с urllib3 1.x. В таком случае мейнтейнеру стоит рассмотреть изменение диапазона, чтобы пользователи могли перейти на безопасную версию.

Но даже здесь невозможность легко обновить уязвимую зависимость до исправленной версии сама по себе не означает, что уязвимость находится в вашей библиотеке.

Что мы об этом думаем

Основная мысль Сета логична – автоматически поднимать нижнюю границу версии после каждой публикации CVE не стоит. Уже созданный lock-файл от изменения метаданных чужой библиотеки не обновится, а при новом разрешении зависимостей менеджер пакетов обычно и так выберет наиболее свежую из подходящих версий. Согласно PEP 440, при наличии нескольких допустимых вариантов установочному инструменту следует предпочесть последний из них.

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

Однако изменение urllib3>=2 на urllib3>=2.6.3 нельзя назвать бессмысленным. Оно запрещает менеджеру пакетов выбирать более ранние версии, а если другая зависимость требует, например, urllib3>=2,<2.6.3, разрешение завершится ошибкой вместо установки уязвимой версии. Ни PEP 440, ни PEP 508 не требуют, чтобы границы версий определялись исключительно совместимостью.

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

Поэтому такой пул-реквест мы бы не стали ни автоматически отклонять вслед за Сетом, ни принимать только из-за наличия записи CVE. Сначала необходимо установить:

— затрагивает ли уязвимость библиотеку при реальном способе использования зависимости;
— какие именно версии считаются затронутыми и исправленными;
— допускает ли текущий диапазон переход на исправленную версию;
— совместима ли исправленная версия со всеми поддерживаемыми средами;
— какие downstream-конфигурации будут намеренно исключены новой нижней границей.

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

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

Подписывайтесь на Codescoring в Telegram, VK, YouTube и Макс.

ссылка на оригинал статьи https://habr.com/ru/articles/1062702/