PyPI закрыл «тихую дыру»: теперь нельзя добавить файл в релиз старше двух недель

Эта уязвимость никогда не эксплуатировалась массово — не потому что её не существовало, а потому что атакующие просто о ней не знали.

Именно так описывает ситуацию Сет Ларсон (Seth Larson), инженер по безопасности Python Software Foundation, в своём свежем посте в блоге PyPI. И именно поэтому исправление было сделано тихо, но своевременно — до того, как кто-то успел воспользоваться брешью в полную силу.

Python Package Index (PyPI) — крупнейший репозиторий пакетов для языка Python — теперь отклоняет загрузку новых файлов в релизы, которым больше 14 дней. Это ограничение введено для того, чтобы злоумышленники не могли «отравить» давно опубликованные и считавшиеся стабильными версии пакетов в случае компрометации токенов публикации или CI/CD-пайплайнов проекта.

ℹ Что такое supply-chain атака?
Supply-chain атака (атака на цепочку поставок) — вид кибератаки, при котором злоумышленник внедряет вредоносный код не напрямую в целевую систему, а через стороннюю зависимость — библиотеку, пакет или инструмент сборки. Разработчик устанавливает, казалось бы, легитимный пакет и получает вместе с ним скрытое вредоносное ПО.

Почему это важно: немая угроза

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

Суть проблемы заключалась в архитектурной особенности PyPI: релизы на платформе исторически были изменяемыми (mutable). Это порождало риски как для безопасности, так и для стабильности: два последовательных запуска pip install могли установить пакеты с одним и тем же номером версии, но разным содержимым.

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


Что послужило триггером: инциденты с LiteLLM и Telnyx

Обсуждение этого поведения началось ещё в январе 2024 года в рамках PEP 740 (цифровые аттестации), а затем возобновилось в марте 2026 года после компрометации популярных пакетов LiteLLM и Telnyx.

24 марта 2026 года группировка «TeamPCP» взломала учётные данные публикации на PyPI для LiteLLM — широко используемой библиотеки для маршрутизации запросов к различным провайдерам LLM. Злоумышленники опубликовали две версии с бэкдором (1.82.7 и 1.82.8), внедрив вредоносный код непосредственно в распространяемые wheel-файлы.

Вредоносные пакеты оставались доступными примерно 40 минут до карантина со стороны PyPI, за это время они успели набрать десятки тысяч загрузок.

⚠ Что делал вредоносный код LiteLLM
После активации зловред реализовывал трёхступенчатую атаку: сбор учётных данных, попытка бокового перемещения по Kubernetes-кластерам и установка постоянного системного бэкдора, который периодически запрашивал дополнительные нагрузки.

Оба пакета — LiteLLM и Telnyx — были скомпрометированы из-за использования «изменяемой ссылки» в GitHub Action Trivy.


Как было принято решение о новом правиле

Изначально предложение было отложено, поскольку некоторые проекты добавляли файлы к уже опубликованным релизам для поддержки новых версий Python. Однако анализ показал, что такая практика встречается крайне редко: из 15 000 наиболее популярных пакетов лишь 56 загружали wheel-файлы с поддержкой Python 3.14 спустя 14 и более дней после первоначального релиза.

Вопрос был вынесен на Packaging Summit в рамках PyCon US 2026 инженером PyPI по безопасности Майком Фидлером (Mike Fiedler). Участники пришли к общему консенсусу: вполне приемлемо требовать от мейнтейнеров публикации новой версии пакета для поддержки новых релизов Python, вместо изменения существующих.

После получения данных и достижения консенсуса Сет Ларсон выпустил патч, который был влит 8 июля 2026 года.


Схема атаки, которую закрывает новое правило


sequenceDiagram
    participant Attacker as Злоумышленник
    participant Token as Украденный токен PyPI
    participant PyPI as PyPI
    participant User as Разработчик

    Attacker->>Token: Компрометирует токен публикации
    Attacker->>PyPI: Загружает вредоносный файл в старый стабильный релиз
    Note over PyPI: До нового правила: файл принят
    User->>PyPI: pip install пакет==1.0.0
    PyPI->>User: Возвращает «отравленный» wheel-файл
    User->>User: Заражение системы

С введением нового ограничения третий шаг этой цепочки становится невозможным: PyPI отклонит загрузку в релиз старше 14 дней вне зависимости от наличия валидного токена.


Что меняется для разработчиков и мейнтейнеров

СценарийДо измененияПосле изменения
Добавить wheel для новой ОС в старый релиз✅ Разрешено❌ Запрещено (если релиз > 14 дней)
Исправить метаданные в свежем релизе✅ Разрешено✅ Разрешено (в первые 14 дней)
Публикация нового релиза для новой версии Python✅ Разрешено✅ Рекомендуемый путь
Атака через старый стабильный релиз✅ Технически возможно❌ Заблокировано
💡 Совет для мейнтейнеров
Если вам нужно добавить поддержку новой версии Python (например, Python 3.14) для уже существующего пакета, теперь правильный путь — опубликовать новый патч-релиз (например, перейти с 1.2.3 на 1.2.4) вместо добавления wheel-файла в старую версию. Это соответствует лучшим практикам семантического версионирования (SemVer).

Что дальше: Upload 2.0 API и PEP 694

Пользователям пока не стоит полагаться на это поведение как на гарантированную семантику: официальные определения для концепции «релиз больше не принимает новые файлы» и соответствующие API ещё не стандартизированы. Эти понятия будут закреплены после стандартизации «Upload 2.0 API» и «Staged Previews» в рамках PEP 694.

В будущем Upload 2.0 API предоставит семантику для релизов, которые «закрыты» в противовес «открытым».

📝 Практический пример: как проверить свой токен

Чтобы снизить риск компрометации публикации пакетов, рекомендуется:

  1. Использовать Trusted Publishers (OIDC) вместо долгоживущих токенов
  2. Регулярно ротировать токены PyPI
  3. Ограничивать область действия токена конкретным пакетом
  4. Настроить двухфакторную аутентификацию на аккаунте PyPI
# Проверить текущие токены через CLI
pip install twine
twine check dist/*

# Переключиться на Trusted Publishers через GitHub Actions
# (токен не нужен вообще — авторизация через OIDC)

Контекст: волна атак на экосистему Python в 2026 году

2026 год стал годом роста как количества публикуемых пакетов, так и объёма вредоносного ПО — в особенности атак типа «watering hole» на пользователей PyPI. Обратная сторона популярности Python: чем шире экосистема, тем привлекательнее она для злоумышленников.

Атаки группировки TeamPCP затронули не только LiteLLM: кампания Hades стала значительной эскалацией по сравнению с предшествующей Mini Shai-Hulud (май 2026 года), которая затронула TanStack, Mistral AI, UiPath и более 160 пакетов.

В мае 2026 года три вредоносные версии официального Python SDK Microsoft для durabletask были опубликованы на PyPI. Скомпрометированный пакет незаметно скачивал и запускал нагрузку весом 28 КБ, которая похищала учётные данные AWS, Azure, GCP, Kubernetes, менеджеров паролей и более 90 конфигураций инструментов разработчика.

Все эти инциденты объединяет одно: атака становилась возможной благодаря компрометации токена публикации PyPI или учётной записи мейнтейнера. Примечательно, что ни один из пострадавших репозиториев не использовал Trusted Publishing (OIDC), что не позволило бы загрузить пакет напрямую по токену вне CI/CD-пайплайна.


Итог

Это изменение защитит пользователей Python и сократит объём «очистительной» работы, которую приходится выполнять администраторам PyPI при компрометации проектов.

Новое правило PyPI — небольшое, но точечное и эффективное: оно закрывает вектор атаки, о котором злоумышленники могли просто ещё не знать. Это классический пример превентивной безопасности: устранить возможность до того, как ею воспользуются.

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