Разбор инцидента LiteLLM 1.107.0: как увели релизный конвейер и почему подпись не помогла


21 октября 2026 года на PyPI вышел litellm==1.107.0 со стилером учётных данных, а образы Docker с тем же тегом оказались заражены. LiteLLM — один из самых популярных open-source LLM-шлюзов: он хранит ключи ко всем вендорам моделей, которыми пользуется команда. Для LiteLLM это второй такой случай за год: в марте 2026 были скомпрометированы версии 1.82.7 и 1.82.8.
Нас инцидент не затронул: у себя мы используем собственный шлюз, KTMS. Но разбирать его интересно независимо от того, какой шлюз у вас. Самое поучительное здесь не стилер, а то, как он прошёл через релизный конвейер, который после мартовского инцидента специально укрепляли.
Расследование продолжается. Факты ниже взяты из отчёта BerriAI (обновление 21.10, 20:30 UTC), разбора StepSecurity, Wiz и Socket. Перед публикацией сверить с последним обновлением отчёта.
Хронология
Время UTC, 21 октября 2026, по отчёту BerriAI:
Время | Событие |
|---|---|
10:08:29 | Защита веток отклоняет неавторизованную правку приватного релизного workflow |
10:08:34 | Токеном привилегированного аккаунта защиту обходят и применяют правку |
10:46:39 | Изменённый workflow запускает staging-сборку из репозитория-двойника |
11:08:53 | Стартует продакшен-конвейер |
11:11:18 | Токен второго разработчика проходит шаг ручного одобрения |
11:39:51 | Семь заражённых wheel-файлов загружены на PyPI |
11:46:56 | Публикуются образы Docker; теги |
11:53:49 | Токеном третьего разработчика удаляются записи о запусках workflow |
12:06 | О компрометации сообщают в issue BerriAI/litellm#47826 |
12:09–12:24 | Изменённый workflow откатывают |
~13:23 | PyPI убирает |
13:51 | Теги на GHCR недоступны, Docker Hub ещё отдаёт заражённый образ |
14:20 | Все затронутые теги Docker Hub удалены или перенаправлены на чистые образы |
От первой правки workflow до публикации прошло полтора часа. Заражённый пакет пролежал на PyPI около 1 ч 45 мин, образ под плавающими тегами Docker Hub — около 2 ч 30 мин.
Официальный тег v1.107.0 в исходном репозитории чистый: артефакты собраны из чужого кода. Клиентов приватного enterprise-реестра, по словам BerriAI, инцидент не затронул.

Как устроена атака
Что было до атаки
После мартовского инцидента BerriAI перестроила выпуск релизов:
релизный кандидат, тесты, сборка артефактов в приватном репозитории, повторные тесты;
публикация через Trusted Publishers с отдельного аккаунта;
неизменяемые теги Docker для релизов;
подписанные образы Docker.
Это заметно лучше среднего по open source. И всё же атака прошла.
Пять шагов атакующего
Окно расширенных прав. За день до атаки двум инженерам временно выдали права на публикацию, чтобы выпустить бэкпорты исправлений.
Три набора токенов. У атакующего были OAuth-токены GitHub CLI трёх разработчиков. Одним можно было запускать workflow, вторым он прошёл шаг ручного одобрения, третьим стёр записи о запусках.
Обход защиты веток. Первую попытку правки релизного workflow защита отклонила. Через пять секунд токеном владельца организации защиту отключили и принудительно запушили изменение.
Подмена источника. Изменённый workflow собирал код не из
BerriAI/litellm, а из двойникаBerri-AI/litellm, отличающегося одним дефисом.Настоящая подпись. Конвейер подписал и опубликовал артефакты как обычно. Проверка подписи показала бы легитимный релиз.

Откуда взялись токены
Это пока не подтверждено. Версия BerriAI: атакующий попал во внутренний деплой LiteLLM, открытый в интернет, вероятно через недавно закрытую уязвимость. Этот деплой писал в логи тела ответов, и в них могли оказаться OAuth-токены GitHub CLI разработчиков. Во внутреннем деплое нашли неожиданный аккаунт с безобидным именем. Как именно атакующий получил доступ и использовал ли он аккаунт для чего-то ещё, BerriAI ещё выясняет.
Что делала нагрузка
Запуск без импорта
В site-packages попадали три файла: cook.pth, cook.py и sitecustomize.py. Файлы .pth Python обрабатывает при каждом старте интерпретатора, а sitecustomize импортирует автоматически. Поэтому для заражения было достаточно установленного пакета: import litellm не нужен, хватит любого python в том же окружении. Даже pip show litellm снова запускает стилер.
Что собиралось
По разбору StepSecurity:
пароли, cookies и история браузеров;
SSH-, GPG- и TLS-ключи;
учётные данные AWS, Azure, gcloud, Kubernetes, Docker, GitHub, реестров пакетов, файлы
.env;учётные данные из метаданных облачного инстанса;
секреты CI и инфраструктуры: Vault, Jenkins, Terraform;
переменные окружения всех доступных процессов;
конфигурации и токены более 20 AI-инструментов для разработки;
история shell и баз данных;
при заданном
DATABASE_URL— база прокси LiteLLM целиком: ключи вендоров, конфигурации моделей, master key.
Образ litellm-database выгружал базу прокси уже при первом старте.
Отправка и второй этап
Архив шифровался AES-256-CBC с ключом, обёрнутым RSA-4096, и уходил обычным HTTP POST на http://www.litellm.dev/healthcheck. Домен зарегистрировали 13 октября, за восемь дней до атаки.
На Linux и macOS sitecustomize.py запускал отдельный демон, который держал /tmp/remote_code_loader.lock, раз в час читал файл-«тайник» в GitHub и выполнял код с указанного там хоста. Аккаунт с тайником удалили, но StepSecurity предупреждает: если кто-то зарегистрирует то же имя заново, заражённые хосты начнут исполнять его код. На Windows загрузчик не работал, а стилер работал.
Поскольку архив зашифрован, по сетевым логам нельзя понять, что именно ушло с конкретного хоста.
Кто затронут и как проверить
Затронуты:
любой хост или CI-раннер, где ставился
litellm==1.107.0, в том числе черезpip install litellmбез версии в окне 11:39–13:23 UTC;любой контейнер из образа
v1.107.0, а также изlatest,main-latest,main-stable, скачанных с 11:46 до 14:20 UTC.
Чистыми названы 1.106.x и более ранние, а также 1.107.0rc1 и 1.107.0rc2.
Не запускайте Python на подозрительном хосте. Проверяйте файловую систему и lock-файлы:
grep -rn "litellm.*1\.107\.0" --include="requirements*.txt" --include="*.lock" --include="pyproject.toml" .
find / \( -name cook.pth -o -name cook.py -o -name 'cook*.pyc' -o -name 'sitecustomize*.pyc' \
-o -name 'litellm-1.107.0*' \) 2>/dev/null
ls -l /tmp/remote_code_loader.lock /tmp/.pc_once_* 2>/dev/null
docker images --digests | grep -i litellm
# заражённый индекс: sha256:1bbcb1443bc962e45f5aad53fe3d2c30600e0379715fb6c664cdd3ac126c339e
Признаки в сети: запросы к litellm.dev и www.litellm.dev. Запрос на путь /codepy означает, что выполнялся второй этап. Найденный файл подтверждает заражение, но отсутствие файлов не доказывает, что хост чист.
Что делать
По рекомендациям BerriAI и StepSecurity:
Убить процесс, держащий
/tmp/remote_code_loader.lock, и заблокироватьlitellm.devна DNS или на выходе из сети.Пересобрать образы и виртуальные окружения с нуля, а не чистить три файла.
Закрепить
litellm==1.106.2или образ по дайджесту, удалить1.107.0из зеркал и реестров.Если 1.107.0 работал на хосте с выходом в интернет, считать, что данные ушли. Сменить всё, до чего хост мог дотянуться: роль инстанса, облачные, SSH- и CI-ключи, токены реестров, Kubernetes и Vault, токены AI-инструментов, любые секреты из окружения процессов.
Для прокси с базой — ещё все ключи вендоров из базы и конфигураций моделей, master key, salt key, виртуальные ключи и пароль из
DATABASE_URL.Поискать в логах
litellm.devи/codepy, в AWS проверить CloudTrail наGetCallerIdentityот роли инстанса около времени установки и на использование её ключей снаружи вашей инфраструктуры. Проверить расход у вендоров моделей.
Уроки
1. Подпись подтверждает происхождение, а не безопасность
Образы LiteLLM были подписаны, и вредоносные тоже. Подпись говорит: «этот артефакт собрал вот этот конвейер». Если конвейер захвачен, подпись остаётся валидной. Помогает закрепление идентичности подписи на конкретный workflow и тег, но настоящая защита — контроль над тем, кто может этот workflow менять. Это вопрос прав доступа, а не криптографии.
2. Ручное одобрение не работает, если аккаунт одобряющего украден
Правило «двух ключей» защищает от ошибки одного человека, но не от атакующего, у которого есть токены обоих. Одобрение имеет смысл, если оно требует фактора, которого нет в токене: аппаратного ключа, отдельного устройства.
3. Временное расширение прав — самое опасное окно
Атака случилась на следующий день после того, как права на публикацию раздали шире. Временные права стоит выдавать с автоматическим сроком истечения и отзывать сразу после использования.
4. Токен владельца организации обходит всё
Защита веток сработала, но токен владельца её просто отключил. Аккаунты владельцев не должны работать с CLI-токенами повседневно, а изменения защиты веток и релизных workflow должны вызывать алерт.
5. Логи с телами ответов — хранилище секретов
Если сервис пишет в лог полные запросы и ответы, туда попадает всё, что в них оказалось, включая чужие токены. Для LLM-шлюза это особенно опасно: через него идут промпты, в которые пользователи вставляют что угодно.
6. Внутренние сервисы не должны смотреть в интернет
Внутренний деплой, открытый в интернет, по версии BerriAI стал точкой входа. «Внутренний» должно значить «доступен только из внутренней сети».
7. Плавающие теги усиливают ущерб
Два с половиной часа под latest лежал заражённый образ. Все, кто деплоил по тегу и перезапускался в это окно, получили его автоматически. Деплой по дайджесту и задержка обновлений на сутки-двое отсекли бы этот сценарий.
Почему шлюз — особенно ценная цель
LLM-шлюз стоит на пути каждого запроса к модели и хранит ключи ко всем вендорам сразу: OpenAI, Anthropic, Gemini, Bedrock и другим. Скомпрометированный шлюз раскрывает их все, а через логи ещё и содержимое промптов. Поэтому к любому шлюзу стоит задать несколько вопросов:
Что исполняется при старте и откуда оно приходит?
Сколько у него зависимостей и как они закреплены?
Кто может менять его релизный workflow и к чему привязана подпись?
Хранятся ли ключи вендоров зашифрованными и где лежит ключ шифрования?
Пишет ли он в логи тела запросов и ответов?
Куда он может ходить по сети?
Открытые вопросы
На момент обновления отчёта BerriAI оставались без ответа:
как атакующий попал во внутренний деплой;
как именно были получены три токена разработчиков и токен владельца;
использовался ли неожиданный аккаунт для чего-то, кроме доступа к логам;
как называется файл-маркер, по которому можно понять, что отправка состоялась.
Обновим статью, когда BerriAI опубликует продолжение.
Чек-лист
Деплойте по дайджесту, а не по тегу.
latestв продакшене не нужен.Не ставьте релиз в день выхода.
Ставьте из lock-файла с хэшами (
pip install --require-hashes).Ограничьте исходящий трафик шлюза адресами вендоров.
Держите ключи вендоров зашифрованными, а ключ шифрования отдельно от базы и бэкапов.
Проверяйте подпись с закреплённой идентичностью и помните, что она гарантирует только происхождение.
Не пишите в логи тела запросов и ответов, а если пишете — считайте логи секретом.
Распишите ротацию заранее: что менять при компрометации шлюза и сколько это займёт.
Если вы сами выпускаете open source: временные права с автоистечением, аппаратные ключи для одобрения релизов, алерты на изменения защиты веток и релизных workflow.
Источники
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.