ой ой ой, опасный контейнер!
ой ой ой, опасный контейнер!

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-сборку из репозитория-двойника Berri-AI/litellm

11:08:53

Стартует продакшен-конвейер

11:11:18

Токен второго разработчика проходит шаг ручного одобрения

11:39:51

Семь заражённых wheel-файлов загружены на PyPI

11:46:56

Публикуются образы Docker; теги v1.107.0, latest, main-latest, main-stable указывают на заражённый индекс

11:53:49

Токеном третьего разработчика удаляются записи о запусках workflow

12:06

О компрометации сообщают в issue BerriAI/litellm#47826

12:09–12:24

Изменённый workflow откатывают

~13:23

PyPI убирает 1.107.0

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. И всё же атака прошла.

Пять шагов атакующего

  1. Окно расширенных прав. За день до атаки двум инженерам временно выдали права на публикацию, чтобы выпустить бэкпорты исправлений.

  2. Три набора токенов. У атакующего были OAuth-токены GitHub CLI трёх разработчиков. Одним можно было запускать workflow, вторым он прошёл шаг ручного одобрения, третьим стёр записи о запусках.

  3. Обход защиты веток. Первую попытку правки релизного workflow защита отклонила. Через пять секунд токеном владельца организации защиту отключили и принудительно запушили изменение.

  4. Подмена источника. Изменённый workflow собирал код не из BerriAI/litellm, а из двойника Berri-AI/litellm, отличающегося одним дефисом.

  5. Настоящая подпись. Конвейер подписал и опубликовал артефакты как обычно. Проверка подписи показала бы легитимный релиз.

не теряйте токены, братья
не теряйте токены, братья

Откуда взялись токены

Это пока не подтверждено. Версия 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:

  1. Убить процесс, держащий /tmp/remote_code_loader.lock, и заблокировать litellm.dev на DNS или на выходе из сети.

  2. Пересобрать образы и виртуальные окружения с нуля, а не чистить три файла.

  3. Закрепить litellm==1.106.2 или образ по дайджесту, удалить 1.107.0 из зеркал и реестров.

  4. Если 1.107.0 работал на хосте с выходом в интернет, считать, что данные ушли. Сменить всё, до чего хост мог дотянуться: роль инстанса, облачные, SSH- и CI-ключи, токены реестров, Kubernetes и Vault, токены AI-инструментов, любые секреты из окружения процессов.

  5. Для прокси с базой — ещё все ключи вендоров из базы и конфигураций моделей, master key, salt key, виртуальные ключи и пароль из DATABASE_URL.

  6. Поискать в логах 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 и другим. Скомпрометированный шлюз раскрывает их все, а через логи ещё и содержимое промптов. Поэтому к любому шлюзу стоит задать несколько вопросов:

  1. Что исполняется при старте и откуда оно приходит?

  2. Сколько у него зависимостей и как они закреплены?

  3. Кто может менять его релизный workflow и к чему привязана подпись?

  4. Хранятся ли ключи вендоров зашифрованными и где лежит ключ шифрования?

  5. Пишет ли он в логи тела запросов и ответов?

  6. Куда он может ходить по сети?

Открытые вопросы

На момент обновления отчёта BerriAI оставались без ответа:

  • как атакующий попал во внутренний деплой;

  • как именно были получены три токена разработчиков и токен владельца;

  • использовался ли неожиданный аккаунт для чего-то, кроме доступа к логам;

  • как называется файл-маркер, по которому можно понять, что отправка состоялась.

Обновим статью, когда BerriAI опубликует продолжение.

Чек-лист

  1. Деплойте по дайджесту, а не по тегу. latest в продакшене не нужен.

  2. Не ставьте релиз в день выхода.

  3. Ставьте из lock-файла с хэшами (pip install --require-hashes).

  4. Ограничьте исходящий трафик шлюза адресами вендоров.

  5. Держите ключи вендоров зашифрованными, а ключ шифрования отдельно от базы и бэкапов.

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

  7. Не пишите в логи тела запросов и ответов, а если пишете — считайте логи секретом.

  8. Распишите ротацию заранее: что менять при компрометации шлюза и сколько это займёт.

  9. Если вы сами выпускаете open source: временные права с автоистечением, аппаратные ключи для одобрения релизов, алерты на изменения защиты веток и релизных workflow.

Источники