Key Takeaway
CVE-2026-42208 is a critical SQL injection in the LiteLLM proxy: an unauthenticated Authorization header reached a database query in versions 1.81.16 up to, but excluding, 1.83.7. Upgrade to 1.83.7 or later, treat any internet-reachable vulnerable instance as exposed, rotate virtual keys, the master key and provider credentials, and keep the gateway off the public internet.
The first question this advisory forces is which of your servers run a LiteLLM proxy, because an LLM gateway is the one service that keeps every upstream API key in a single database. The advisory describes exactly the worst case for that design: no login, no special route, one header on an ordinary chat completions request.
This post is a defender's read of CVE-2026-42208. It rests on the GitHub advisory, the CIRCL record that mirrors NVD and CISA data, Sysdig's honeypot write-up, a Cloud Security Alliance research note and LiteLLM's own production docs. It contains no exploit payloads, only what you need to patch, detect and shrink the blast radius. Anything I could not read in a source is left out.
What the advisory actually says
LiteLLM's proxy checks the API key on every request. In the affected versions, a database query used during that check put the caller-supplied key value into the query text instead of passing it as a separate parameter. An unauthenticated attacker could send a crafted Authorization header to any LLM route, the advisory names POST /chat/completions, and reach that query through the error-handling path. The result is read access to the proxy database and possibly write access, which the advisory says can lead to unauthorized access to the proxy and the credentials it manages.
The table collects the facts I could confirm directly. One source disagrees on the upper bound of the range, which I come back to in the trade-offs section.
| Fact | What the source says | Source |
|---|---|---|
| Affected versions | litellm on PyPI, 1.81.16 or later and below 1.83.7 | GitHub advisory GHSA-r75f-5x8p-qvmc |
| Fixed version | 1.83.7 (tag v1.83.7-stable); the caller-supplied value is now always passed as a separate parameter | GitHub advisory |
| Severity | CVSS 4.0 score 9.3 and CVSS 3.1 score 9.8, both Critical, network attack, no privileges, no user interaction | CIRCL record of the CVE |
| Weakness class | CWE-89, SQL injection | GitHub advisory |
| Exploitation status | Added to the CISA KEV catalog on 2026-05-08 with a due date of 2026-05-11; CISA's SSVC entry records exploitation as active and technical impact as total | CIRCL record, CISA KEV |
| Credit | Tencent YunDing Security Lab | GitHub advisory |
Why one header reached the database
The bug class is old and boring, which is the point. A lookup that should have been parameterised was built by pasting text into SQL, and the text came from the one thing an unauthenticated caller fully controls, the key they present. Authentication code runs before any authorisation, so a flaw there is reachable by everyone. That is why the CVSS vector says no privileges and no user interaction.
# Generic illustration of the bug class. NOT LiteLLM's source: the
# advisory does not publish it.
# Wrong: the caller's string becomes part of the SQL text, so whatever
# the caller sends is parsed as SQL.
query = f"SELECT * FROM tokens WHERE token = '{presented_key}'"
row = await conn.fetchrow(query)
# Right: the SQL text is fixed, the value travels separately and can
# never be parsed as SQL. This is what the advisory says 1.83.7 now does.
row = await conn.fetchrow(
"SELECT * FROM tokens WHERE token = $1",
presented_key,
)The advisory does not publish the vulnerable source, so the snippet below is a generic illustration of the pattern and its fix, not LiteLLM's code. What the advisory does say is that the query is reached through the error-handling path, and that setting disable_error_logs to true under general_settings removes the route unauthenticated input takes to it.
The 1.83.7 release notes list one change that matches the description, a pull request titled use parameterized query for combined_view token lookup. The notes do not tie it to the CVE, so treat the match as likely rather than confirmed. The same release lists other security items, among them blocking arbitrary command execution through the MCP stdio transport and input validation on management endpoints, so upgrading buys more than this one fix.
What attackers went after, and the blast radius
Sysdig ran a honeypot and recorded the first injection attempt on 2026-04-26 at 04:24 UTC, 36 hours and 7 minutes after the advisory was indexed in the GitHub Advisory Database. The maintainers had published their own advisory on 2026-04-20, so measured from that moment the gap was about 127 hours. Both numbers are correct; they just start the clock at different events. Sysdig saw 17 attempts against three tables, then a second source address replaying the same payload set 21 minutes after the first activity.
| Table | What it holds | What an attacker gains |
|---|---|---|
| LiteLLM_VerificationToken | Virtual API keys, including the master key | Impersonation of any proxy user, and admin access with the master key |
| litellm_credentials | Stored upstream provider credentials; Sysdig lists OpenAI, Anthropic and Bedrock | Calls straight to providers, bypassing the proxy's spend limits, filters and audit logging |
| litellm_config | Proxy environment variables, which Sysdig says typically include the PostgreSQL connection string, master key, callback webhook URLs and cache backends | A second foothold: the database itself, the webhooks and the cache |
Sysdig is careful about what it did not see. There was no authenticated follow-on activity, no key minting and no reuse of extracted credentials, so it describes the activity as attempted extraction, not confirmed compromise. That is a statement about one honeypot. It says nothing about your instance, and the Cloud Security Alliance note makes the practical point that data reads may not leave application-layer log entries at all.
The same note states that for organisations holding organisation-level or account-root provider credentials in the proxy, the blast radius is comparable to a full cloud account compromise. I would not put it more strongly than that, but I would not put it more weakly either: the gateway is a secrets store with an HTTP front door.
Patch, workaround or fence: what each one does
Only the upgrade fixes the flaw. The other options reduce exposure while you schedule it, and the CSA note is explicit that the config workaround reduces risk without fixing the injection. The table lines them up by what they actually stop.
| Option | Stops the injection? | Cost | Source |
|---|---|---|---|
| Upgrade to 1.83.7 or later | Yes, the value is passed as a separate parameter | A deploy, plus reading the release notes for the other changes | GitHub advisory, release notes |
| disable_error_logs: true under general_settings | No. It removes the path unauthenticated input uses to reach the query | LLM exceptions stop being stored in the database | GitHub advisory, CSA note |
| Reverse proxy that rejects odd Authorization values | Not the flaw, but it blocks the known request shape before it reaches the app | A filter you must keep correct; an allowlist can reject legitimate keys | Sysdig mitigations |
| Network fence: VPN, API gateway or internal-only listener | No, but unauthenticated strangers can no longer reach the route | Clients outside the fence need a path in | Sysdig, CSA note |
The config workaround is less of a sacrifice than it sounds. LiteLLM's own production page already recommends disable_error_logs: True, so that exceptions do not bloat the spend logs during a provider outage, and says to send them to your logging stack instead. If you were following that page, you had the workaround on before the CVE existed.
# Read the version the RUNNING process reports, not the one in requirements.txt.
pip show litellm | grep -i '^Version'
docker ps --format '{{.Names}} {{.Image}}' | grep -i litellm
docker exec <container> python -c "import importlib.metadata as m; print(m.version('litellm'))"
# Upgrade; the advisory's patched range is 1.83.7 and above.
pip install --upgrade "litellm>=1.83.7"
# Bridge only, if you cannot deploy today (config.yaml):
# general_settings:
# disable_error_logs: trueA patched version number is not proof you are patched. Read the version the running container reports, not the one in your requirements file, and confirm every replica restarted. A single forgotten worker on the old image keeps the door open.
Detection you can add to nginx today
Sysdig's detection guidance is to watch POST requests to /chat/completions whose Bearer token starts with a quote-terminated prefix, flag Authorization values containing SQL keywords or table names, and note that the user agent Python/3.12 aiohttp/3.9.1 appeared on every injection request. It also lists two source addresses, 65.111.27.132 and 65.111.25.67, both in AS200373. Treat those as indicators from one campaign, not a complete list.
Do not log the Authorization header to hunt for this. That writes every valid key into a log file. The config below flags only a boolean, using an allowlist for what a Bearer key may contain. Real keys are plain tokens, so anything with a quote, parenthesis or space is wrong by construction. This is my own design, built from Sysdig's mitigation of blocking quotes and parentheses. Test it against your real clients first.
# /etc/nginx/conf.d/llm-gateway.conf
# An allowlist for what a Bearer key may contain. Anything else, such as a
# quote, a parenthesis or a space inside the token, is flagged.
# Tune the character class to the key formats your own clients send.
map $http_authorization $auth_malformed {
default 1;
"" 0; # no header: the app rejects it
"~^Bearer [A-Za-z0-9._~+/=-]+$" 0;
}
# Log the verdict, never the header: logging Authorization would write
# every valid key to disk.
log_format llm_sec '$remote_addr [$time_local] "$request" $status '
'auth_malformed=$auth_malformed ua="$http_user_agent"';
server {
listen 443 ssl;
server_name llm.example.internal;
location / {
access_log /var/log/nginx/llm-gateway.log llm_sec;
if ($auth_malformed) { return 400; } # refuse before the proxy sees it
proxy_pass http://127.0.0.1:4000; # your LiteLLM upstream
}
}Then search the history you already have. The retroactive check is cheap, and Sysdig calls a single matching request before patching a high-confidence indicator.
# Indicators Sysdig reported for one campaign. zgrep reads plain and .gz logs.
zgrep -hE '65\.111\.(27\.132|25\.67)' /var/log/nginx/access.log*
zgrep -h 'Python/3.12 aiohttp/3.9.1' /var/log/nginx/access.log* | grep 'POST /.*chat/completions'
# Your new guard: every refused request since deployment.
grep 'auth_malformed=1' /var/log/nginx/llm-gateway.log | tail -50Hardening the proxy after the patch
Patching removes this bug, not the design problem. The CSA note advises threat-modelling the proxy tier like an identity provider or a secrets manager, in an isolated environment with its own secret store. The config below collects settings that appear in LiteLLM's production and config references. I verified each key against those pages, and the comments say why.
# config.yaml: keys taken from LiteLLM's production and config references.
general_settings:
master_key: os.environ/LITELLM_MASTER_KEY # never a literal in the file
disable_error_logs: true # keeps exceptions out of the DB
trusted_proxy_ranges: ["10.0.0.0/8"] # [] if clients connect directly
database_connection_pool_limit: 10
# allowed_ips: ["203.0.113.10"] # Enterprise licence only
litellm_settings:
request_timeout: 600 # default is 6000 seconds
set_verbose: false
json_logs: true
# Environment, set outside the file:
# LITELLM_MODE=PRODUCTION do not auto-load credentials from a local .env
# LITELLM_LOG=ERROR
# LITELLM_SALT_KEY=... never change it after adding models, or
# stored API keys become unreadable
# DATABASE_URL=postgresql://user:pass@host:5432/litellm?sslmode=verify-full&sslrootcert=/certs/global-bundle.pemTwo more points belong outside the YAML. First, scope the provider keys the proxy stores: project-level keys with minimum permissions and spend limits, and for Bedrock an IAM role scoped to specific model ARNs, as the CSA note recommends. Second, allowed_ips exists but the docs mark it as an Enterprise feature, and they do not say how it reads client addresses behind a load balancer. I would rely on a network fence you control instead of assuming it behaves.
Where this goes wrong: the trade-offs and the named failure
The failure that made this CVE expensive is architectural, not the missing parameter. A gateway stood up to simplify key management became the one place where a single unauthenticated bug yields every provider credential, and it often sits on a public address because developers outside the VPN need it. Sysdig's recommendation to inventory AI gateway deployments, including ones stood up outside formal security review, is aimed at exactly this.
Clean logs do not mean clean. Data reads through SQL injection may leave no application-layer entry, so for an internet-reachable instance that ran an affected version the CSA note says to treat it as potentially compromised and rotate everything. Patching alone does not invalidate a key that has already been copied.
There are smaller traps. The workaround hides error visibility in the database, so plan where exceptions go instead. The version bound also differs between sources: the advisory, the CVE record and the CSA note all say below 1.83.7 is affected, while an AhnLab advisory writes 1.83.7 and below. I followed the advisory, but the safe move is to run 1.83.7 or newer and read the release notes for what is current. And the exploitation clock runs fast: CISA gave federal agencies three days from the KEV listing, 2026-05-08 to 2026-05-11, to apply mitigations.
What to do on Monday
Work in this order. The first two steps take minutes and tell you whether the rest is urgent.
- Inventory every LiteLLM proxy you run or that a team stood up without telling you, and record the version each one actually reports.
- Upgrade anything in the affected range to 1.83.7 or later, and restart every replica. If you cannot, set disable_error_logs to true and put the proxy behind a fence today.
- If an affected instance was internet-reachable, rotate in this order: the master key, virtual keys, then every stored provider credential. Revoke at the provider, not just in the LiteLLM UI.
- Add the nginx guard, then search historic logs for the two campaign addresses and the user agent Sysdig reported.
- Check provider billing and usage for traffic you cannot explain, especially from outside your proxy.
Last, shrink the next blast radius: scoped provider keys, spend caps at the provider, and a listener that is not on the public internet. The patch buys time; the architecture decides what the next bug costs.
The rule I take from CVE-2026-42208: an LLM gateway is a credentials vault that happens to proxy requests, so patch it like one, fence it like one, and assume that anything it stored while exposed has been read. Upgrade to 1.83.7 or later, rotate on suspicion, and keep the workaround as a bridge rather than a destination.
Sources and further reading
- GitHub Security Advisory GHSA-r75f-5x8p-qvmc: SQL injection in Proxy API key verification
- CIRCL Vulnerability-Lookup: CVE-2026-42208 record with CVSS, SSVC and KEV data
- Sysdig: targeted SQL injection against LiteLLM's authentication path, observed 36 hours after disclosure
- Cloud Security Alliance research note on the LiteLLM AI proxy SQL injection
- LiteLLM docs: production setup recommendations
- LiteLLM docs: proxy config settings reference
- LiteLLM release notes: v1.83.7-stable
- LiteLLM docs: IP address filtering
- CISA Known Exploited Vulnerabilities catalog entry for CVE-2026-42208

