LiteLLM's security page tells you to run a supported stable release, but no -stable GitHub release has been cut since v1.83.14-stable.patch.3 on 2026-05-07 while mainline sits at 1.95.0, so following that advice pins you at 1.83.14, below the 1.84.0 floor that CVE-2026-49468 (CVSS 9.8) and CVE-2026-59822 require. The project published fifteen unique advisories carrying thirteen CVE IDs in the first seven months of 2026, against fourteen advisories across all of 2024 and 2025 combined. Two of them sit in CISA KEV: CVE-2026-42208 (unauthenticated SQL injection through the Authorization header, added 2026-05-08 with a three-day due date) and CVE-2026-42271 (MCP stdio command execution, added 2026-06-08). With LITELLM_MASTER_KEY unset and no JWT or OAuth2 configured, the auth builder returns an INTERNAL_USER token for any caller and authorization checks return early, which the source comments describe as unauthenticated by configuration. Set the floor at 1.84.0, prove auth is enforced with a 401, and pin a digest rather than a tag.
LiteLLM's security documentation gives you seven pieces of advice. The second reads: run a supported stable release. Follow it literally and you land on v1.83.14-stable.patch.3, published 2026-05-07. That release sits below 1.84.0, the floor set by CVE-2026-49468 (CVSS 9.8, authentication bypass via Host header injection) and CVE-2026-59822 (MCP authentication bypass via OAuth2 passthrough fallback). The vendor's documented guidance, followed exactly, pins you underneath the version floor its own advisories demand.
That is not a rhetorical point. Pulling the 300 most recent releases from the GitHub Releases API across three pages, spanning 2025-12-17 to 2026-08-03 and checked on 2026-08-03, returns 13 tags carrying -stable. None is newer than 2026-05-07, while mainline sits at 1.95.0. One caveat: the ghcr.io/berriai/litellm tag list returns a thousand unordered tags and truncates, so I cannot rule out a newer -stable container artifact. Re-run both checks at your deployment date.
LiteLLM security hardening deserves more scrutiny than most infrastructure, because of what the process holds: every provider API key the organization owns, plus the virtual-key database. Compromising the gateway is not a lateral move toward the credentials. It is the credentials. What follows is the gate we run before one goes in front of production keys. We audited the serving tier one layer down in our vLLM inference server hardening guide.
Start here: which LiteLLM version are you actually running
Get the number from the running artifact, not the chart.
python -c "import litellm; print(litellm.__version__)" # inside the container crane digest ghcr.io/berriai/litellm:main-latest # what your tag points at
Then map it against the advisories that move your floor. The GitHub Advisory API returns 16 records for 2026, one explicitly titled a duplicate of CVE-2026-40217, leaving fifteen unique advisories carrying thirteen distinct CVE IDs. Two carry no CVE identifier, which is why a CVE-keyed scanner will not give you this list.
The floor is 1.84.0. It is the highest first-patched version in the set, required independently by CVE-2026-49468 and CVE-2026-59822. If your change board wants one number to approve, that is it, and the derivation is what makes it defensible rather than arbitrary.
For scale: those fifteen advisories landed in seven months, against fourteen across all of 2024 and 2025 combined, four on a single day (2026-07-22). Nor is this niche, at 55,466 stars and 668,801,041 PyPI downloads in the trailing 30 days (a count that may include mirror traffic, so read it as reach, not as unique installs). Scores in the table are CVSS 3.1 base scores as recorded by NVD and the GitHub advisory API; where it says "not scored," the advisory record carries no CVSS value at all.
| Advisory | CVSS 3.1 base | Affected | Fixed in | What it gives an attacker |
|---|---|---|---|---|
| CVE-2026-59822 | not scored | < 1.84.0 | 1.84.0 | MCP auth bypass via OAuth2 passthrough fallback |
| CVE-2026-49468 | 9.8 | < 1.84.0 | 1.84.0 | Auth bypass via Host header injection |
| CVE-2026-47101 | 8.8 | < 1.83.14 | 1.83.14 | internal_user mints keys for routes its role forbids |
| CVE-2026-47102 | 8.8 | < 1.83.10 | 1.83.10 | User writes its own user_role through /user/update |
| CVE-2026-40217 | 8.8 | >= 1.81.8, < 1.83.10 | 1.83.10 | Sandbox escape in the custom-code guardrail exec() path |
| CVE-2026-59819 | not scored | < 1.83.10 | 1.83.10 | Local file read via request-supplied OIDC file references |
| CVE-2026-42208 | 9.8 | >= 1.81.16, < 1.83.7 | 1.83.7 | Unauthenticated SQL injection in API key verification |
| CVE-2026-42271 | 8.8 | >= 1.74.2, < 1.83.7 | 1.83.7 | Command execution via the MCP stdio test endpoints |
| CVE-2026-42203 | not scored | >= 1.80.5, < 1.83.7 | 1.83.7 | Server-side template injection in /prompts/test |
| CVE-2026-59820 | not scored | < 1.83.7 | 1.83.7 | Arbitrary file write via path traversal in Skills extraction |
| CVE-2026-35030 | not scored | < 1.83.0 | 1.83.0 | Auth bypass via OIDC userinfo cache collision (JWT auth only) |
| CVE-2026-35029 | not scored | < 1.83.0 | 1.83.0 | Privilege escalation via an unrestricted config endpoint |
| GHSA-69x8 (no CVE) | not scored | < 1.83.0 | 1.83.0 | Password hash exposure, pass-the-hash through /v2/login |
| CVE-2026-59821 | not scored | < 1.82.0 | 1.82.0 | Custom-code guardrail production endpoints skip safety checks |
| GHSA-5mg7 (no CVE) | not scored | 1.82.7 to 1.82.8 | none | Published packages containing credential-harvesting malware |
The default that ships an open gateway
The most valuable finding here is not a CVE. It is the behavior of a stock configuration, and it lives in the source, not the docs.
In litellm/proxy/auth/user_api_key_auth.py on main (a 2,959-line file, read 2026-08-03), the auth builder has a branch at lines 1403 to 1416 that fires when master_key is None. It returns a UserAPIKeyAuth object with user_role set to LitellmUserRoles.INTERNAL_USER, both for any supplied api_key and for no api_key at all. The next branch carries the comment # only require api key if master key is set.
That alone would be a permissive default. What makes it an open gateway is the second half, at lines 2142 to 2153 in the common_checks path, where the maintainers describe it in their own words:
# No-auth dev mode: master_key unset AND no JWT/OAuth2 auth
# configured. The builder returns an INTERNAL_USER token for any
# api_key; the proxy is unauthenticated by configuration.
# Running common_checks would block every admin route on these
# deployments where that was previously not the contract. If any
# authn is enabled (JWT, OAuth2, OAuth2-proxy), authz must run.
if master_key is None and not (
general_settings.get("enable_jwt_auth", False)
or general_settings.get("enable_oauth2_auth", False)
or general_settings.get("enable_oauth2_proxy_auth", False)
):
returnAuthorization checks return early, admin routes included. With LITELLM_MASTER_KEY unset and no identity provider wired in, anyone who can open a TCP connection to the port is an INTERNAL_USER with authorization disabled. This file moves quickly, so re-read it against the commit you deploy.
The vendor security page never states this. Its only master-key line says to store the master and salt keys in a secret manager rather than config.yaml, which is correct and beside the point. So prove enforcement, from a network position a real caller would have:
PROXY=https://litellm.internal.example
# Admin route, no Authorization header. 401 is the only acceptable answer.
curl -s -o /dev/null -w '%{http_code}\n' "$PROXY/user/list"
# Inference path with a garbage key. Also 401.
curl -s -o /dev/null -w '%{http_code}\n' -X POST \
-H 'Authorization: Bearer sk-not-a-real-key' \
"$PROXY/chat/completions" -d '{"model":"gpt-4o","messages":[]}'A 200 on the first means the proxy minted you an INTERNAL_USER token, and the next section explains why that is not the low-privilege position it sounds like. Put both in the deploy pipeline. Same argument as role-based access control for AI applications: a role that skips its checks is a label, not a control.
Two CISA KEV entries, and both due dates have passed
Most components never appear in the Known Exploited Vulnerabilities catalog. LiteLLM has two, verified against catalog 2026.08.03.
The three-day window on the SQL injection is the detail to notice. The KEV norm sits closer to three weeks, so three days is CISA saying the exploitation evidence was not ambiguous.
Both due dates are behind us, which changes what an unpatched instance is. Under BOD 22-01 a federal civilian agency running one is out of compliance now rather than at the next assessment, and the same holds for contractors who took the directive on through contract language. If you sell into that market, or operate under any framework that imports KEV as its remediation trigger, this is a live finding with a clock that started in May, not a backlog ticket. It is also the cheapest question an auditor can ask, because the catalog is public and your version is not hard to fingerprint.
CVE-2026-42208 breaks the usual mental model of gateway risk. Per NVD, a database query used during proxy API key checks mixed the caller-supplied key value into the query text instead of passing it as a separate parameter, so an unauthenticated attacker could send a crafted Authorization header to any LLM API route (POST /chat/completions, for example) and reach the query through the proxy's error-handling path. Exploitation arrives on the normal inference path, not an admin route. A gateway with the master key correctly set and the admin UI off the internet was still reachable, because the injection lives inside the code that checks the key.
NVD scores it 9.8 under CVSS 3.1, the GitHub CNA 9.3 under CVSS 4.0. Disclosure dates disagree across sources (GitHub advisory 2026-04-24, the vendor's post says 29 April, NVD 2026-05-08), so call it late April 2026. Threat researchers reported targeted exploitation beginning 36 hours and 7 minutes after disclosure, against production instances rather than honeypots, using tooling that fingerprints LiteLLM by probing /health then tests the injection. That is the argument against a monthly-only window for critical severity: 36 hours fits inside almost every change-approval process.
Close one remediation gap yourself. The vendor's guidance is to upgrade and, on an affected version exposed to untrusted networks, to review Postgres query history using a helper query it published. It does not tell you to rotate credentials. If an unauthenticated attacker had SQL access to the table holding your virtual keys, rotation is not optional.
CVE-2026-42271 is the second entry. Two endpoints for previewing an MCP server before saving it, POST /mcp-rest/test/connection and POST /mcp-rest/test/tools/list, accepted a full server configuration in the request body including the command, args and env fields used by the stdio transport. Calling them with a stdio configuration spawned the supplied command as a subprocess on the proxy host with the proxy's privileges. KEV's description carries the scoping detail: any authenticated user, including holders of low-privilege internal-user keys, could run arbitrary commands. Combine that with the default-auth behavior above and "authenticated" stops being a barrier. Researchers chained it with the Starlette Host-header flaw to reach an unauthenticated execution path; one team assessed the chain at CVSS 10.0, a score absent from NVD. The transport decisions in our MCP server security hardening checklist apply directly here.
| CVE | KEV title | Added | Due | Window |
|---|---|---|---|---|
| CVE-2026-42208 | BerriAI LiteLLM SQL Injection | 2026-05-08 | 2026-05-11 | 3 days |
| CVE-2026-42271 | BerriAI LiteLLM Command Injection | 2026-06-08 | 2026-06-22 | 14 days |
The privilege-escalation chain: default role to host RCE
CVE-2026-47101, CVE-2026-47102 and CVE-2026-40217 are usually listed as three unrelated bugs. They are not. Researchers published them as a single chain, scored 9.9 combined, walking from the default low-privilege role to code execution on the gateway host. As a list it is three CVSS 8.8 issues you might defer. As a chain it is the reason 1.83.14 was ever a meaningful line.
Step one, CVE-2026-47101. /key/generate accepted a caller-supplied allowed_routes value without validating it against the caller's role, so an internal_user mints a key with allowed_routes: ["/*"]. PUT /key/update, POST /key/regenerate and POST /key/service-account/generate shared the gap.
Step two, CVE-2026-47102. POST /user/update and POST /user/bulk_update passed the request body through an internal parameter-update helper with no field filtering, so user_role was written straight through. The caller sets user_role: "proxy_admin" on itself.
Step three, CVE-2026-40217. POST /guardrails, PUT /guardrails/{id} and POST /guardrails/test_custom_code compiled user-supplied Python with exec() while leaving __builtins__ populated. The published proof of concept reaches __builtins__['__import__']('os').system(...) and spawns a reverse shell, and a regex deny-list on the test endpoint was bypassable through bytecode rewriting. The fix came in two parts: a February 2026 change adding proxy_admin checks and clearing __builtins__, then an April change replacing the deny-list with a RestrictedPython sandbox. All three steps are cleared by v1.83.14-stable, published 2026-05-02.
Correct one common piece of advice: "disable the custom-code guardrail exec path" is not a mitigation, because those endpoints needed no special configuration and were reachable by default. The fix is the version. What you can audit is what is already configured, and that matters because of how the chain ends. Callbacks loaded from config.yaml under litellm_settings.callbacks do not appear in the admin UI, so a planted callback is invisible to exactly the UI-only review a compliance walkthrough performs. An attacker holding proxy_admin can also register MCP callbacks that tamper with agent responses: a man-in-the-gateway position over every prompt and tool call transiting the proxy, which invalidates any data-protection story assuming the gateway is trustworthy, as we describe in preventing data leakage in AI applications.
Three audits, run against the live instance rather than the repository:
# 1. Virtual keys with wildcard route access
curl -s -H "Authorization: Bearer $LITELLM_MASTER_KEY" "$PROXY/key/list" \
| jq '.. | objects | select(.allowed_routes) | {key_alias, user_id, allowed_routes}'
# 2. Every account holding proxy_admin
curl -s -H "Authorization: Bearer $LITELLM_MASTER_KEY" "$PROXY/user/list" \
| jq '.. | objects | select(.user_role == "proxy_admin") | {user_id, user_email}'
# 3. Callbacks and guardrails the admin UI will not show you
grep -A20 'litellm_settings:' config.yaml | grep -E 'callbacks|guardrails'Item three is the one people skip. Run it against the config the pod actually mounted, not the one in Git.
BadHost: when the auth bypass lives in Starlette, not LiteLLM
CVE-2026-49468 is a CVSS 9.8 authentication bypass in LiteLLM, and NVD names the code directly: the auth layer derived the effective route from request.url.path in litellm/proxy/auth/auth_utils.py::get_request_route(), which Starlette reconstructs from the Host header, so a crafted Host could make the auth gate evaluate a different route from the one FastAPI dispatched. Fixed in 1.84.0. NVD notes it applies under specific conditions, so do not assume it is trivially reliable.
The underlying primitive is a Starlette flaw named BadHost, CVE-2026-48710, demonstrable in one request:
GET /protected HTTP/1.1 Host: example.com/health?x=
The request routes to /protected, because routing dispatches on the real path. But request.url.path returns /health, because Starlette rebuilds the URL as scheme plus host header plus path and nothing filtered / and ? out of the header. Path-based auth middleware inspects the wrong route and waves the request through.
Several details about BadHost circulate incorrectly, so here is the verified record. The affected range is every Starlette version below 1.0.1, which OSV records as introduced at 0, covering 191 released versions from 0.1.0 onward. CVSS is 6.5 medium (AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N), recorded identically by NVD, the GitHub CNA and OSV; higher scores attributed to the discovering audit team are not published anywhere I could verify. Starlette 1.0.1 shipped 2026-05-21 and ignores Host headers containing invalid characters instead of using them for URL construction. The disclosure site states Starlette alone has more than 400,000 dependents on GitHub.
Now the part that changes what you do. LiteLLM 1.95.0 already declares starlette<2.0,>=1.0.1 under its proxy extra, alongside fastapi<1.0,>=0.136.3 and uvicorn<1.0,>=0.33.0, so a fresh install cannot resolve a vulnerable Starlette. "Add Starlette >= 1.0.1 to your requirements" is stale advice. The residual risk is the on-prem pattern specifically: old lockfiles, vendored wheels, frozen air-gapped images, and constraint files that pin Starlette below 1.0.1 and quietly win against the declared range. Audit the resolved tree in the artifact you deploy, not the range in pyproject.toml:
# in the running container, not in CI pip list --format=freeze | grep -Ei '^(starlette|fastapi|uvicorn|litellm)='
In your own ASGI middleware, read scope["path"] and never request.url.path.
The March supply chain compromise: 40 minutes was enough
On 2026-03-24, LiteLLM 1.82.7 and 1.82.8 went live on PyPI at 10:39 UTC carrying credential-harvesting malware. PyPI quarantined them roughly 40 minutes later. Forty minutes sounds survivable until you remember that CI systems pull continuously and unattended.
The two versions were not identical, and the difference decides your triage. Version 1.82.7 carried an obfuscated payload injected into litellm/proxy/proxy_server.py that executes on import. Version 1.82.8 carried that payload plus a litellm_init.pth file, and executable lines in .pth files run during interpreter startup, so on 1.82.8 the payload fires whenever any Python process starts on that host. A machine that installed 1.82.8 and never ran the proxy is still compromised.
The payload harvests environment variables, SSH keys, AWS, GCP and Azure credentials, Kubernetes tokens, Docker configs, shell history, database passwords and CI/CD secrets, encrypts them with an AES-256 session key wrapped under RSA-4096, then exfiltrates to a lookalike domain. Persistence polls a second attacker domain, downloads a binary to /tmp/pglog and executes it.
litellm_init.pth # filesystem artifact, 1.82.8 only models.litellm[.]cloud # exfil domain, unaffiliated with the project 83.142.209[.]203:8080 # exfil endpoint /tmp/pglog # second-stage payload
Initial access generalizes past this incident. Per the project's own post, the attackers bypassed the official CI/CD workflows and uploaded directly to PyPI, and the project believes the compromise originated from an open-source vulnerability scanner in its own CI/CD security scanning workflow, whose stolen credentials reached the publishing pipeline. A security tool inside the release pipeline became the entry point into it.
Rotation is the remediation, not upgrading. The vendor's guidance is to rotate all credentials, inspect filesystems for litellm_init.pth, and audit version history across every environment, with 1.83.0 rebuilt through a new pipeline. If either version ever touched a host, every provider key, cloud credential, SSH key and kubeconfig reachable from it is burned. Upgrading to 1.95.0 does nothing about a key that left in March.
A version pin is not a control; a hash is. Both malicious versions now return zero files from the PyPI JSON API, so a version-pinned rebuild resolves nothing today, but you also cannot retroactively verify what you pulled unless your lockfile recorded hashes. Use --require-hashes and an internal index keeping immutable copies. Several widely used agent frameworks list LiteLLM as a direct dependency, so teams that never evaluated this gateway had it installed anyway: the failure surface we cover in AI supply chain and package hallucination risk and the MCP supply chain and tool-poisoning audit.
Network placement and database privilege
Everything above assumes the attacker can reach the port.
Never expose the proxy directly. Terminate at nginx or Envoy and require that layer to normalize or reject malformed Host headers. That one control neutralizes both the LiteLLM Host-header bypass and the Starlette primitive underneath it, independent of version.
Split the listeners. Management routes (/user/*, /key/*, /config/*, /guardrails/*, /mcp-rest/*) have no business on a broadly-reachable listener. Enforce an explicit edge allowlist.
Give the proxy a least-privilege database role. CVE-2026-42208 was reachable through the auth path itself, so the mitigation that survives a repeat is not "patch faster", it is that the proxy's SQL identity cannot read or alter the tables holding admin accounts and audit records. Confirm the grant rather than assuming the connection string implies it.
Keep egress deny-by-default. A gateway that can only reach the provider endpoints you named cannot exfiltrate to a lookalike domain, which is what the March payload attempted.
If you have not committed to a gateway yet, the tiering and spend tradeoffs are a separate decision from security posture, covered in choosing between LiteLLM, Portkey and Kong AI Gateway. For the governance layer in front of MCP servers, see our MCP gateway comparison.
Day-2 operations
Pin a digest, not a tag. The project's own docs say to avoid :latest, :main-latest and :main-stable in production, and all three exist on the registry. A rolling tag means the artifact you validated in staging is not necessarily the one that starts in production.
image: ghcr.io/berriai/litellm@sha256:<digest-you-validated> # not :main-latest
Track the advisory feed, not a CVE scanner. Two of the fifteen unique 2026 advisories carry no CVE identifier, one the malware incident, and a CVE-keyed scanner reports both as clean. One scheduled job and a diff closes the gap:
gh api /advisories --paginate -X GET \
-f ecosystem=pip -f affects=litellm \
--jq '.[] | select(.published_at > "2026-01-01") |
"\(.published_at[0:10]) \(.cve_id // "NO-CVE") \(.severity) \(.summary)"'Set a cadence that matches the release rate. This project ships several releases a week, so tracking head is not viable under change approval and lagging a quarter stacks multiple auth bypasses. A monthly window with a same-week path for critical severity is the sustainable trade.
What to watch, in rough order of signal value:
| Signal | Why it matters | Alert on |
|---|---|---|
Host headers containing / or ? | The BadHost and CVE-2026-49468 exploit signature | Any occurrence |
Requests to /mcp-rest/test/* | Preview endpoints legitimate clients never call | Any request from outside ops |
POST /user/update with user_role in the body | The self-promotion step of the escalation chain | Any occurrence |
Keys created with allowed_routes containing /* | The wildcard step of the escalation chain | Any creation event |
| 401 rate at the proxy versus request rate at the gateway | A divergence means traffic is bypassing your edge | Non-zero divergence |
| Egress denials from the gateway segment | Exfiltration attempts, or a provider allowlist too tight | Sustained non-zero |
| Config or callback changes outside a deploy | Post-compromise persistence the admin UI hides | Any occurrence |
The gate: run this before a gateway holds production keys
Work top to bottom. Items 1 through 4 would have blunted every issue in this post.
-stable line, which has not moved since 2026-05-07 as verified 2026-08-03. Re-verify releases and registry tags at your deployment date.LITELLM_MASTER_KEY (it must begin with sk-) and LITELLM_SALT_KEY, then prove enforcement** by curling an admin route with no Authorization header and asserting a 401 in the pipeline.:latest, :main-latest or :main-stable.Host headers, and keep /user/*, /key/*, /config/*, /guardrails/* and /mcp-rest/* off any broadly-reachable listener.allowed_routes containing /*, every account for user_role: proxy_admin, and config.yaml for litellm_settings.callbacks and registered guardrails.** The admin UI hides the last one.litellm_init.pth.None of this is really a CVE-count problem. A gateway gets deployed with the assumptions of an internal service and attacked with the assumptions of an internet-facing one, and the stock configuration takes the internal reading: trusted network, trusted caller. Two of the criticals above only reach that severity because of it. Settle placement and auth enforcement first, and the version floor is something an ordinary patch window can carry.
Particula Tech runs this gate for regulated on-premise deployments: version floor, source-level verification of what the auth path enforces, network placement, database role separation, and the rotation call after a supply chain event. More in our AI security pillar.
FAQ
Quick answers to the questions this post tends to raise.




