SafeDep нашла в LiteLLM 1.107.0 на PyPI похититель данных и бэкдор

Компания SafeDep, занимающаяся безопасностью, сообщает, что релиз LiteLLM 1.107.0 на PyPI содержал похититель учётных данных и бэкдор для удалённого выполнения кода. Все семь wheel-пакетов релиза (для Linux, macOS и Windows) устанавливают в site-packages три файла: cook.pth, cook.py и sitecustomize.py. Оба компонента срабатывают при запуске любого процесса Python на машине, даже если он никогда не импортирует litellm. В исходном коде на GitHub этих файлов нет, а PyPI уже удалил релиз. Версии 1.106.2, 1.107.0rc1 и 1.107.0rc2 чистые; сопутствующие пакеты, которые закрепляет 1.107.0 (litellm-proxy-extras==0.4.110 и litellm-enterprise==0.1.78), тоже чистые.
Что делает вредонос. Файл cook.pth содержит строку import cook, то есть импортирует cook.py, а это похититель. Он собирает SSH-ключи, облачные учётные данные, секреты CI/CD, токены ИИ-инструментов, пароли из браузеров и ключи LLM-провайдеров из базы прокси LiteLLM. Данные шифруются (случайный ключ AES-256-CBC, который, в свою очередь, шифруется встроенным открытым ключом RSA-4096 по схеме RSA-OAEP) и отправляются POST-запросом на www.litellm[.]dev/healthcheck. После успешной отправки в /tmp создаётся маркер, поэтому сбор идёт один раз на хост. Файл sitecustomize.py запускает фоновый демон. Раз в час (LOOP_INTERVAL = 3600 секунд) демон читает адрес сервера из файла domain.txt в GitHub-репозитории атакующего, заново запускает встроенную копию похитителя (у неё маркера «один раз на хост» нет) и скачивает с этого сервера по пути /codepy код, который выполняет через exec(). Меняя domain.txt, атакующий может перенаправить демон на новый сервер.
Как имплант попал в релиз. В pyproject.toml архива исходников (sdist) версии 1.107.0 добавлена одна строка в разделе сборки maturin: data = "cook". Она копирует папку cook/ в wheel, а pip ставит её в site-packages. Сама папка cook/ в sdist не включена, поэтому эта строка, единственное видимое изменение. Ни в одном публичном коммите BerriAI/litellm нет ни этой строки, ни папки cook/. Правка также возвращает версию 1.107.0 и закрепляет сопутствующие пакеты на значениях rc2, так что сравнение зависимостей с rc2 не показывает ничего необычного. В списке RECORD wheel-пакета указаны: cook.pth (12 байт), cook.py (22252 байта, внутри сжатый и закодированный в base64 код примерно на 61 КБ Python) и sitecustomize.py (25509 байт).
Релиз не совпадает со своим тегом. Тег v1.107.0 указывает на коммит 66c8f5b (06:59 UTC), в котором в pyproject.toml стоит версия 1.108.0. SafeDep сравнила каждый файл sdist с деревьями git по хешам блобов: sdist соответствует более позднему коммиту main a8113b0 (08:50 UTC) плюс пять изменённых файлов и папка cook/. Четыре из пяти файлов, Cargo.toml с добавленной строкой readme = "README.md" (такая же правка есть в легитимных 1.106.2 и rc2), пятый, pyproject.toml.
Как собраны wheel-пакеты. Они похожи на результат обычного релизного конвейера BerriAI: в файлах WHEEL указан maturin 1.9.4, как и в легитимных релизах; записи RECORD для файлов импланта корректны, что говорит об отсутствии правок после сборки; метки времени в zip показывают отдельные нативные сборки, macOS в 11:20, Linux в 11:21, Windows в 11:37 UTC. В Windows-пакете все 2804 файла .py имеют окончания строк CRLF, включая три файла импланта, а в Linux-пакете таких нет, так выглядит результат git checkout на Windows с преобразованием окончаний строк. Публичные запуски GitHub Actions релиз не собирали и не публиковали: SafeDep проверила все 539 запусков с 09:30 до 12:30 UTC. В 11:50 UTC, примерно через 10 минут после загрузки на PyPI, бот litellm-dispatcher[bot] опубликовал GitHub-релиз v1.107.0; такая же задержка была у 1.106.2. Подтверждений происхождения (provenance attestations) на PyPI для 1.107.0, 1.106.2 и rc2 нет.
Вывод SafeDep. С умеренной или высокой уверенностью компания считает, что приватный релизный конвейер BerriAI собрал эти wheel-пакеты из изменённого атакующим git-источника. Атакующий мог использовать скомпрометированные учётные данные мейнтейнера или бота, приватную ветку или зеркало либо ссылку, из которой конвейер берёт сборку. Кража токена PyPI с использованием с собственных машин атакующего хуже согласуется с фактами. Подтвердить причину могут только журналы конвейера BerriAI.
Текущее состояние. В 13:40 UTC (все времена в отчёте относятся к 2026-10-21) аккаунт GitHub letters9813 и его репозиторий стали отдавать 404, примерно через два часа после релиза. Теперь бэкдор не находит адрес сервера и ничего не делает. Но хосты, на которых демон отработал до удаления, могли получить и выполнить код второго этапа. SafeDep адрес сервера восстановить не смогла.
Ключевые факты
- По данным SafeDep, все семь wheel-пакетов LiteLLM 1.107.0 на PyPI (Linux, macOS, Windows) ставят три файла: cook.pth, cook.py и sitecustomize.py; они срабатывают при запуске любого процесса Python, даже без импорта litellm.
- Похититель собирает SSH-ключи, облачные учётные данные, секреты CI/CD, токены ИИ-инструментов, пароли браузеров и ключи LLM-провайдеров из базы прокси LiteLLM и шифрует их перед отправкой на www.litellm[.]dev.
- Бэкдор раз в час берёт адрес сервера из файла в GitHub-репозитории атакующего, снова запускает похититель и выполняет код, скачанный с сервера.
- В публичных исходниках BerriAI/litellm имплант отсутствует, тег v1.107.0 указывает на коммит с версией 1.108.0, а 539 проверенных запусков GitHub Actions релиз не публиковали; PyPI релиз удалил.
- Чистыми названы версии 1.106.2, 1.107.0rc1 и 1.107.0rc2; GitHub-аккаунт атакующего удалён в 13:40 UTC, но хосты, где демон успел отработать, могли получить код второго этапа.
Почему это важно
LiteLLM, пакет, через который приложения обращаются к LLM-провайдерам, поэтому на машинах с ним обычно лежат ключи к ИИ-сервисам и облакам. По описанию SafeDep, вредонос срабатывает не при импорте litellm, а при запуске любого процесса Python на хосте: приложения, pip, языковых серверов IDE, задач CI. Кроме того, имплант не виден в публичном репозитории: единственный след в sdist, одна строка в pyproject.toml, а сама папка cook/ в нём отсутствует. Это пример того, как публичный код и опубликованный на PyPI пакет могут расходиться.
Кому это важно
Тем, кто ставил или обновлял LiteLLM в окне, когда релиз 1.107.0 был доступен на PyPI: разработчикам, командам с LiteLLM-прокси (похититель забирает ключи LLM-провайдеров из его базы), владельцам CI-конвейеров и образов, куда мог попасть этот пакет. Источник не сообщает, сколько установок было и как долго релиз оставался на PyPI до удаления. Также тем, кто отвечает за безопасность цепочки поставок и публикацию пакетов на PyPI.
Как это применить
Проверьте, не была ли где-либо установлена версия litellm==1.107.0 (машины разработчиков, CI, контейнеры, серверы с прокси). По данным SafeDep, чистыми являются 1.106.2, 1.107.0rc1 и 1.107.0rc2, а релиз 1.107.0 с PyPI удалён. Признаки заражения: файлы cook.pth, cook.py и sitecustomize.py в site-packages, маркер /tmp/.pc_once_72129118c59f5636c0bb, файл блокировки /tmp/remote_code_loader.lock, обращения к www.litellm[.]dev. Из описания следует, что на заражённых хостах стоит исходить из компрометации перечисленных секретов, а при запуске демона до 13:40 UTC 21 октября 2026 года, и из возможного выполнения кода второго этапа. Полные списки индикаторов компрометации и хеши смотрите в оригинальном отчёте SafeDep.
Можно ли доверять
Источник, отчёт самой SafeDep (автор указан как Kunal Singh), с техническими подробностями: содержимым RECORD, сравнением sdist с деревьями git по хешам блобов, проверкой 539 запусков GitHub Actions. Причину компрометации SafeDep оценивает с умеренной или высокой уверенностью и прямо оговаривает, что подтвердить её могут только журналы релизного конвейера BerriAI. В использованном тексте отчёта нет заявления BerriAI или мейнтейнеров LiteLLM; для выводов о причине стоит дождаться их комментария.
Риски и подводные камни
Содержание кода второго этапа (codepy) неизвестно: SafeDep не смогла восстановить адрес сервера, поэтому нельзя оценить, что получили хосты, где демон отработал до удаления GitHub-аккаунта. После удаления аккаунта бэкдор не находит адрес сервера и бездействует, но это не отменяет возможной утечки данных, собранных ранее. Похититель встроен в копию внутри бэкдора без маркера «один раз на хост», поэтому на заражённой машине сбор мог повторяться каждый час. Сравнение зависимостей с rc2 ничего необычного не показывает, а подтверждений происхождения на PyPI для 1.107.0, 1.106.2 и rc2 нет, обычные проверки могут пропустить подобную подмену.