Безопасность зависимостей в Python: как не поймать пакет-вредонос
Опечатка в имени пакета, скомпрометированный мейнтейнер или закладка в новом релизе — три способа занести чужой код в свой проект. Разбираем, как выстроить защиту на всех этапах.

Вы пишете pip install reqeusts вместо requests — и вместо HTTP-клиента ставите пакет-двойник, который при импорте отправляет переменные окружения на чужой сервер. Это не гипотеза: PyPI регулярно чистит тайпсквоттинг, а инциденты с закладками в релизах случаются несколько раз в год. Разберём, где именно ломается доверие к зависимостям и как выстроить защиту от установки до CI, чтобы одна опечатка или один скомпрометированный аккаунт мейнтейнера не превратились в утечку секретов.
Материал — практический разбор для тех, кто уже собирает проекты на Python 3.11+ и хочет понять, какие меры реально снижают риск, а какие создают иллюзию безопасности. Углубляться в устройство самого PyPI не будем — фокус на цепочке поставки в вашем репозитории.
Откуда берётся вредоносный код
Три разных класса угроз, и защита от каждого — своя. Смешивать их — частая ошибка: люди прикручивают сканер уязвимостей и считают, что закрыли тайпсквоттинг, хотя это несвязанные вещи.
- Тайпсквоттинг и подмена имени — злоумышленник публикует пакет с именем, похожим на популярное (
python-requestsвместоrequests,djnagoвместоdjango). Ставится по опечатке или по невнимательности при копировании из плохого гайда. - Dependency confusion — если у вас есть внутренний приватный пакет
internal-utils, а кто-то опубликует пакет с тем же именем на публичном PyPI с версией выше, ваш сборщик при неправильной настройке может подтянуть публичный. - Компрометация легитимного пакета — реальный популярный пакет получает вредоносный релиз: угнали аккаунт мейнтейнера, влили malware через PR, добавили закладку в один патч-релиз. Здесь имя правильное, репутация настоящая — спасает только фиксация версий и проверка того, что вы ставите ровно то, что проверяли.
Пиннинг версии по номеру защищает от неожиданного обновления, но не от того, что кто-то перезальёт артефакт под тем же номером. От этого защищает только хеш.
Фиксация: версии мало, нужен хеш
Запись requests==2.31.0 в requirements фиксирует номер версии. Но PyPI до определённых политик допускал ситуации, а зеркала и приватные индексы допускают до сих пор, когда под одним номером лежит другой файл. Гарантию даёт хеш содержимого. Формат --require-hashes в pip требует, чтобы для каждого пакета был указан sha256, и падает, если хеш не совпал.
# requirements.txt с хешами (pip 23.x+)
requests==2.31.0 \
--hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \
--hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1
urllib3==2.1.0 \
--hash=sha256:55901e917a5896a10ce0b3fb6bce8f2a4d1c2a8f42a9cae5e0da9e34cd1ad3b8Собирать такой файл руками не нужно. Инструменты компиляции зависимостей делают это сами: pip-tools (pip-compile --generate-hashes) и uv (uv pip compile --generate-hashes) читают верхнеуровневые требования и раскладывают полное дерево с хешами. Устанавливать потом:
# установка строго по зафиксированным хешам
pip install --require-hashes -r requirements.txtКлючевое поведение: если в дерево прилетает транзитивная зависимость без хеша — установка падает целиком. Это фича, а не баг. Именно так вы узнаёте, что кто-то в цепочке подтянул незапланированный пакет.
Сканирование известных уязвимостей
Отдельная задача — не «злой код», а известные CVE в честных пакетах. Здесь помогает база уязвимостей. Два рабочих варианта под Python:
- pip-audit — официальный инструмент от PyPA. Сверяет установленное окружение или requirements-файл с базой advisories (PyPI Advisory Database и OSV).
- Safety — коммерческий с бесплатным CLI, своя база; для части возможностей нужен аккаунт.
# проверка requirements без установки в окружение
pip-audit -r requirements.txt
# проверка текущего активного окружения
pip-auditЧто pip-audit не делает: он не ищет закладки и не анализирует код пакета. Он знает только про то, что уже кем-то заведено в базу уязвимостей. Свежая закладка, о которой ещё нет advisory, пройдёт мимо. Поэтому сканер — необходимое, но не достаточное звено.
Dependency confusion: закрываем приоритет индексов
Если вы ставите пакеты и с публичного PyPI, и с приватного индекса, критично, как pip выбирает между ними при совпадении имён. Флаг --extra-index-url добавляет дополнительный индекс, но pip будет искать пакет по всем индексам и может выбрать версию с публичного, если она выше. Это и есть дыра для dependency confusion.
Безопаснее разделять источники явно. В новых версиях pip есть параметр стратегии поиска по индексам — уточните актуальное имя и поведение в разделе документации pip про index priority для вашей версии, потому что дефолты менялись. Общий принцип: приватные пакеты тянуть только из приватного индекса, а не полагаться на то, что публичного двойника «наверное, нет».
Сравнение подходов к фиксации
| Подход | От чего защищает | Чем платите |
|---|---|---|
Без пиннинга (requests) | Ни от чего; сборка невоспроизводима | Любое обновление меняет то, что стоит |
Пиннинг версии (requests==2.31.0) | От случайного обновления | Не защищает от перезалитого артефакта |
Пиннинг + хеши (--require-hashes) | От подмены артефакта под тем же номером | Нужен инструмент компиляции; обновление — отдельный шаг |
| Хеши + приватное зеркало | Ещё и от исчезновения пакета и dependency confusion | Инфраструктура: своё зеркало и его поддержка |
Что проверять до установки нового пакета
Перед тем как добавить незнакомую зависимость, минимальная проверка занимает пару минут и отсекает большинство подделок:
- Сверьте имя пакета с официальным репозиторием или документацией проекта, а не с первым результатом поиска. Тайпсквоттинг живёт именно на невнимательности.
- Посмотрите на странице PyPI дату последнего релиза и число загрузок. Пакет-двойник обычно свежий и с околонулевой статистикой.
- Откройте ссылку на исходники (Homepage / Repository). Отсутствие репозитория у пакета, который претендует на популярность, — красный флаг.
- Проверьте, нет ли у пакета скрипта, выполняющегося при установке. Установка из sdist может запускать код на этапе сборки; предпочитайте wheel там, где возможно.
- Прогоните имя через сканер после добавления:
pip-auditпокажет известные проблемы уже установленной версии.
Что ломается на практике
Хеши и editable-установки. --require-hashes несовместим с установкой пакетов в editable-режиме (-e) и с VCS-ссылками без фиксации коммита. Локальную разработку своих пакетов держите отдельно от файла с хешами продовых зависимостей.
Кросс-платформенные хеши. Для пакетов с бинарными колёсами хеш зависит от платформы: колесо под Linux x86_64 и под macOS arm64 — разные файлы с разными sha256. Файл требований должен содержать хеши для всех платформ, где вы собираетесь ставить, иначе установка на другой ОС упадёт. Инструменты компиляции умеют собирать хеши под несколько платформ, но это надо явно запросить.
Сканер молчит — не значит чисто. Пустой вывод pip-audit говорит только об отсутствии известных уязвимостей на момент запуска базы. Свежая закладка в новом релизе не попадёт в advisory мгновенно.
Что настроить дальше
Минимальный рабочий набор для команды: компиляция requirements с хешами через pip-tools или uv, установка в CI и на проде строго с --require-hashes, шаг pip-audit в пайплайне как блокирующая проверка. Следующий уровень — своё зеркало PyPI (например, через devpi или прокси в вашем артефакт-хранилище), чтобы не зависеть от доступности публичного индекса и контролировать, какие пакеты вообще попадают в периметр. Обновления зависимостей вынесите в отдельные PR: так изменение хешей и версий проходит ревью, а не проскакивает вместе с фичей.
Проектная работа: От проблемы до защиты: продукт, этапы, вопросы комиссии
Спросить за 15 ₽Источники: pip-audit — PyPA, pip: Secure installs (require-hashes)
Частые вопросы
У меня старая версия pip, флаг --require-hashes не работает?
pip-audit ничего не нашёл — значит, зависимости безопасны?
Установка с --require-hashes падает на другой ОС, что делать?
Чем uv отличается от pip-tools для этой задачи?
Как защититься от dependency confusion с приватными пакетами?
Стоит ли бояться скриптов, выполняемых при установке?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.