LiteLLM MCP on the KEV List: A Patch Clock for Agent Gateways
The LiteLLM MCP vulnerability hit CISA KEV on Sep 2. Block /mcp/ at the proxy, start the clock at the advisory, and prove a forged Bearer now gets a 401.
Go deeper. Build your own.
On Sep 2, CISA added CVE-2026-59822 to its Known Exploited Vulnerabilities catalog and gave federal agencies until Sep 16 to act. By that morning the fix had been in a LiteLLM release for 111 days and the vendor advisory had been public for 64. Wiz, the firm that found this LiteLLM MCP vulnerability, says its honeypots caught exploitation on Jul 7, eight weeks before the listing. A patch ticket opened on Sep 2 opened late.
The bug lets a made-up Bearer token through the MCP endpoint of a gateway that teams put in front of the tools their agents use. This piece is the runbook for it and for the next one. By Tuesday every agent gateway you run gets a clock that starts at the vendor advisory, a containment rule you can apply in minutes, a forged-token test that runs after every upgrade and must return 401, a log hunt across the whole exposure window, and a rotation list for everything the gateway fronted.
The history is why it deserves its own clock. This is LiteLLM’s third KEV entry of 2026, and the second on an MCP code path. A component that keeps landing on that list should not wait for CISA to tell it what day it is.
Sep 2: CISA lists CVE-2026-59822, LiteLLM’s third KEV entry this year
The advisory is GHSA-7488-6r32-c95q, an MCP authentication bypass through an OAuth2 passthrough fallback, rated High at 8.8 under CVSS 4.0. NVD’s own CVSS 3.1 score is 8.2, so don’t mix the two in a ticket. Every litellm release before 1.84.0 is affected.
When a request to the MCP Streamable HTTP endpoint carried an Authorization header that failed LiteLLM’s key check, a fallback built for OAuth2 passthrough swapped the failed validation for an empty UserAPIKeyAuth() object and let the request continue. The caller could then list and call whatever MCP tools the gateway had configured and reach the services behind them.
The advisory also carries the containment step this runbook opens with: when the upgrade can’t happen right away, switch off the MCP routes, or block /mcp/ and the related MCP endpoints at whatever reverse proxy or API gateway sits in front of LiteLLM.
Screenshot: GitHub, “LiteLLM: MCP Authentication Bypass via OAuth2 Passthrough Fallback” (Jun 30, 2026), captured Sep 21, 2026.
CISA’s catalog entry (added Sep 2, due Sep 16) describes the same flaw in one line, lists ransomware use as unknown, and tells agencies to apply the vendor’s mitigations under BOD 26-04, Prioritizing Security Updates Based on Risk. The entry itself tells no exploitation story.
LiteLLM’s three KEV entries this year:
- CVE-2026-42208, a SQL injection an unauthenticated caller could reach with a crafted Authorization header on any LLM API route. NVD and KEV both list it on May 8; the federal due date was May 11, three days later.
- CVE-2026-42271, command execution through two MCP server-preview endpoints,
POST /mcp-rest/test/connectionandPOST /mcp-rest/test/tools/list. Per NVD and its advisory (Apr 21), they accepted a full stdio server config, command included, and checked only for a valid proxy key with no role check; CISA’s entry adds that even a low-privilege internal-user key could run commands on the host. Fixed in 1.83.7, which requires thePROXY_ADMINrole; added to KEV Jun 8, due Jun 22. On Sep 2 CISA also added a Starlette request-smuggling flaw, CVE-2026-48710, noting it could be chained with this one. - CVE-2026-59822, above.
Two of the three sit on MCP code paths, but not the same path. The June bug lives in the preview endpoints, the September bug in the live tool endpoint. That matters when you write the proxy rule.
An MCP gateway bug hands out tools, not tokens
A model gateway with an auth hole leaks prompts and burns your token budget. An MCP gateway with an auth hole hands a stranger your tools, and each tool holds a credential for something real: a repository, a ticket queue, a database.
Chatbots suggest; agents act, and the gateway is where you gathered the ability to act so you could govern it in one place. The gateway as control plane is still the right design. It also means one CVE in the control plane is a CVE in every connector at once. Neither that piece nor the MCP hardening checklist names a KEV due date or a negative-auth test, and those are what the day after a listing needs.
The fix PR is blunter than the advisory. It says the fail-open, combined with MCP servers marked allow_all_keys=True, gave any unauthenticated caller full tool access on those servers. If you run LiteLLM, that flag is the first column of your blast-radius list.
Step 1: Contain today if the upgrade can’t land today
Containment is two moves, and you want both before lunch on advisory day.
Block the MCP routes at the proxy. The advisory names /mcp/; the June bug’s endpoints live under /mcp-rest/test/. Match on the path, never on the full URL. The fix PR closed a second bypass the advisory doesn’t mention: LiteLLM’s own public-route check looked for .well-known anywhere in the URL, so appending ?.well-known to an MCP route skipped auth. A proxy rule that matches the path ignores query-string tricks.
# Illustrative containment for LiteLLM below 1.84.0. Prefix match on the path only.
# "/mcp" also covers /mcp/ and /mcp-rest/. Adjust if LiteLLM sits under a path prefix.
location ^~ /mcp {
return 403;
}
Take the endpoint off the internet. Bind the listener to a private interface, or narrow the security group to the addresses your agent hosts egress from. If remote agents need MCP, put the gateway behind your VPN rather than in front of the world.
Then look for the second door. A proxy rule guards only traffic that passes through it: a published container port, a Kubernetes Service on a node port, a second load balancer or someone’s port-forward goes straight to LiteLLM. List every path to the listener and close or test each one.
Containment has a cost. Every agent that reaches tools through that gateway loses them until the upgrade lands. Decide in advance who may accept that outage, and log the containment time; it is the first entry on your clock.
Step 2: Start the patch clock at the vendor advisory, not the KEV listing
Here is the 59822 clock with T0 at the advisory:
| Date (2026) | Event | Source | Clock |
|---|---|---|---|
| Apr 25 / Apr 30 | Fix PR #26463 opened / merged | GitHub | T−66 / T−61 |
| May 14 | v1.84.0 released; notes list an MCP OAuth2 fallback fix | GitHub release | T−47 |
| Jun 30 | GHSA published to the LiteLLM repo | GitHub | T0 |
| Jul 7 | Exploitation seen in Wiz honeypots | Wiz (vendor) | T+7 |
| Jul 8 | NVD record published | NVD | T+8 |
| Jul 22 | Reviewed into the GitHub Advisory Database | GitHub | T+22 |
| Sep 2 | Added to KEV | CISA | T+64 |
| Sep 16 | Federal due date | CISA | T+78 |
The advisory is the right T0 because it is the first moment the vendor says, in words a ticket can cite, that a version you run is exploitable and what to do about it. The v1.84.0 release notes were an earlier signal, a line about tightening public-route detection and OAuth2 fallback gating, and teams that read gateway release notes for auth wording got a 47-day head start.
KEV is the latest signal of all. By the listing, Wiz says exploitation had been visible in its honeypots for 57 days. Its honeypot write-up carries the line to pin above the patch queue: “Patch on the assumption that the exploit is already in the wild.”
Three LiteLLM KEV entries in 2026. For CVE-2026-59822 the advisory came 64 days before KEV; the fixed release, 111.
Two things in that table trip teams up. First, one advisory has three publication dates: Jun 30 in the repo, Jul 8 at NVD, Jul 22 in the GitHub Advisory Database. Your dependency scanner keys on one of them, and it may not be the first. Check which, and never let the scanner’s alert date become your clock start.
Second, the discoverer’s timeline disagrees with GitHub on the release date. Wiz lists the fix as released Apr 25, which is the day the fix PR was opened; the release itself is dated May 14. Use the artifact’s date.
Screenshot: Wiz Blog, “Breaking LiteLLM: From Auth Bypass to Cloud Compromise” (Sep 9, 2026), captured Sep 21, 2026.
Write the clock down as policy before the next advisory. This is a starting table, and the numbers are ours, not CISA’s or the vendor’s:
| Trigger on a gateway you run | Internet-reachable | Internal only |
|---|---|---|
| Fixed release with auth or MCP wording, no advisory yet | Upgrade within 14 days | Next maintenance window |
| Vendor advisory: auth bypass or code execution on a route you expose | Contain same day; patch within 7 days | Patch within 14 days |
| Credible report of exploitation | Contain within the hour | Contain same day |
| KEV listing while still open | Incident: contain now, page the owner | Incident |
KEV due dates bind federal agencies (the September entry cites BOD 26-04), and the windows vary: 14, 14 and 3 days for LiteLLM’s three. For everyone else they are a backstop. Pull the feed daily, alert when a vendor you run appears, and compare its dateAdded with your own clock start. If the listing was your first alarm, the clock failed, and that is a finding for the retro.
# Daily backstop: KEV entries for a vendor you run (fields from CISA's KEV JSON feed)
curl -s https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json \
| jq -r '.. | objects | select(.vendorProject? == "BerriAI")
| [.dateAdded, .dueDate, .vulnerabilityName] | @tsv'
Keep one record per component so the clock survives a shift change:
# patch-clock.yaml (illustrative shape; owner, exposure and server names are yours)
- component: litellm-proxy
owner: platform-gateway
exposure: internet # internet | vpn | localhost
mcp_servers_allow_all_keys: [tickets, warehouse-readonly]
advisory: GHSA-7488-6r32-c95q
cve: CVE-2026-59822
clock_start: 2026-06-30 # vendor advisory, never the KEV date
fixed_in: 1.84.0
contained_at: null
patched_at: null
verified_by: forged-bearer-test
kev_added: 2026-09-02 # backstop only
Step 3: Prove the LiteLLM MCP vulnerability is closed with a forged Bearer
A version string says what you deployed. It doesn’t say what answers on the port. After every upgrade, restart and config change, and once a day besides, send a well-formed MCP initialize request with a fabricated Bearer to every exposed route and transport. The pass condition is a 401, not just any 4xx.
A 403 may be your step 1 proxy rule, which says nothing about the gateway. A 404 means the route moved. A 500 means the auth path errored; the fix PR specifically stopped auth errors being masked as 500s, and a gateway that 500s on a forged token is one refactor away from a 200.
The vendor doesn’t say 401 is the correct answer. This is operator practice, and it’s cheap.
Run it in this order: straight at the gateway from inside, with the proxy block still up; then lift the block; then again through the proxy from every vantage point your agents use.
Two cuts: the proxy closes the route, the 1.84.0 fix closes the fallback. OAuth2 pass-through targets still need the upstream to refuse.
Use four token shapes per route: Bearer x (the single-character shape Wiz saw probing its honeypots, there on a models endpoint rather than /mcp/), a random 40-character string, an empty Bearer and no Authorization header at all. Repeat each with ?.well-known appended.
Then one control: the same request with a scoped test key must succeed. Without the control, a gateway that is simply down passes every negative test. The v1.84.0 release also flags breaking changes, so the control catches an upgrade that broke your agents as well.
#!/usr/bin/env bash
# forged-bearer.sh (illustrative). Non-zero exit on any failure; wire it into the deploy.
set -u
GW="${GW:?gateway base URL; one run per vantage point}"
ROUTES=("/mcp/" "/mcp/?.well-known") # add every MCP route and transport your logs show
BODY='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"<your-spec-version>","capabilities":{},"clientInfo":{"name":"forged-bearer-test","version":"1"}}}'
fail=0
probe() { # $1 route, $2 Authorization header line ("" sends none)
curl -s -o /dev/null -w '%{http_code}' --max-time 10 -X POST "$GW$1" \
${2:+-H "$2"} -H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' --data "$BODY"
}
RAND=$(head -c 64 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | head -c 40)
for r in "${ROUTES[@]}"; do
for auth in "Authorization: Bearer x" "Authorization: Bearer $RAND" "Authorization: Bearer" ""; do
code=$(probe "$r" "$auth")
[ "$code" = "401" ] || { echo "FAIL $r [$auth] -> $code, want 401"; fail=1; }
done
done
code=$(probe "/mcp/" "Authorization: Bearer ${CONTROL_KEY:?scoped test key}")
[ "$code" = "200" ] || { echo "FAIL control -> $code, want 200"; fail=1; }
exit $fail
Two limits on scope. The advisory names the Streamable HTTP endpoint and doesn’t say whether any other transport was affected, so test every transport and route prefix your deployment exposes.
Carve out OAuth2 pass-through: 1.84.0 still allows the empty-auth fallback when every target MCP server is configured auth_type: oauth2, by design, because the token is meant to reach the upstream. For those servers the gateway may accept the forged token; the test is whether the upstream refuses it. Make one read-only tool call with the forged token and assert that you get the upstream’s authorization error and no tool result. Mixed targets, unresolvable server names and empty target lists fail closed after the fix, so they belong in the 401 set.
Wire the script into the deploy, not into a wiki page. A non-zero exit blocks promotion and reopens the clock, and a timeout counts as a failure: a test that can’t reach the gateway has proved nothing.
The test is a check, not a wall. The wall is the patched version plus a listener that isn’t on the internet. The suite that contract-tests guard coverage on every harness bump is the natural home for it.
Step 4: Hunt the /mcp/ logs across the whole exposure window
The window is not advisory-to-patch. It runs from the first day a pre-1.84.0 build served MCP on a reachable route to the moment step 3 first passed. The advisory gives no lower bound on affected versions, so for most teams the window starts the day MCP went live on that gateway.
| Signal | Where to look | Why it matters |
|---|---|---|
| 2xx on an MCP route for a Bearer that matches no issued key | Proxy access log joined to your key inventory by hash | The bypass itself |
| One- or two-character Bearer values | Proxy or gateway log, if it records token length or a hash | The probe shape Wiz saw |
.well-known in the query string of an MCP route |
Proxy access log | The second bypass closed by the same PR |
tools/list followed by tools/call from a source no agent host uses |
Gateway request log | Enumeration, then use |
| 500s on MCP auth paths | Gateway error log | Auth failures masked before the fix |
Calls to /mcp-rest/test/ from a non-admin key |
Gateway request log | The June bug, if you ran 1.74.2 through 1.83.6 |
| Actions by the gateway’s upstream credential with no matching agent session | Each upstream service’s own audit log | What the tools were used for |
The last row matters most and gets skipped most. Join every upstream audit event in the window to your agent session records by time and credential; an action no agent of yours requested is the finding.
Most proxies don’t log Authorization values at all, which is correct and also means the first two rows may come back empty. Write that down. If your logs don’t reach back to the start of the window, treat the window as compromised for step 5 rather than as clean.
If the hunt turns up tool calls with shell or file-write reach on agent hosts, stop treating this as a gateway incident. Assume something was left behind and run the eradication proof for unauthored hooks on every host those tools touched.
Step 5: Rotate everything the gateway fronted, after the fix is proven
Order matters. Prove the fix (step 3), rotate, revoke the old credential, then prove the old one fails. Rotating before the fix hands the new credential to the same hole.
| Credential | Rotate when | Proof the old one is dead |
|---|---|---|
| Upstream tokens and keys the gateway holds for MCP servers (repo, tickets, chat, database, cloud) | Always, for anything reachable in the window | A call with the old value gets the upstream’s auth error |
Credentials behind MCP servers marked allow_all_keys=True |
Always, first | Same as above |
| LiteLLM keys issued to agents and teams | When the hunt shows use you can’t attribute, or logs can’t reach back | A request with the old key gets a 401 |
| The LiteLLM master key | Now, if it is still the default | An admin call with the old key fails |
Wiz reports that 9.6% of the 3,074 public LiteLLM instances it examined accepted the default master key or no auth at all, so the last row is not hypothetical. How a gateway should hold upstream secrets in the first place is credential brokering, its own discipline and a different article.
Step 6: Hand census, floors and asserts to the fleet patch drill
Everything else is a drill you may already run. Finding every LiteLLM instance, including the one a team runs in a container on a laptop; setting version floors (1.84.0 for 59822, 1.83.7 for 42271); asserting after deploy that the running version is the one you meant: that is the Plugin4Shell pin-and-verify drill, run with LiteLLM rows. For gateways nobody registered, start with the shadow MCP sweep. The only columns this piece adds are the clock start and the date step 3 last passed.
Where a LiteLLM MCP vulnerability patch clock goes wrong
The second door. The proxy blocks /mcp/, and a node port serves it anyway. Signal: the forged-Bearer test from inside the agent subnet reaches LiteLLM directly and gets a 200.
The half-rolled deploy. New image, old replica. Signal: run the test 20 times through the load balancer and get a mix of 401s and 200s. Any mix is a failure.
The 404 that passed. Someone moved the route and a loosely written negative test went green. Signal: the control request fails too, which is why the control exists. Only a 401 beside a successful control is a pass.
The OAuth2 blind spot. Every target server is auth_type: oauth2, the gateway lets the forged token through by design, and nobody checks the upstream. Signal: the upstream’s own log shows the forged token and something other than a refusal.
The late clock. The ticket’s creation date is the KEV date. Signal: for 59822, a ticket opened after Jul 7 was opened after Wiz saw exploitation, and one opened on Sep 2 was opened by CISA, not by you.
Rotation in the wrong order. New keys went out while the build was still vulnerable. Signal: the rotation timestamp precedes the first passing step 3 run. Rotate again.
Agent gateways are fleet infrastructure, so the patch clock belongs to the fleet
An internet-reachable gateway that fronts private data and write-capable tools holds all three legs of the Rule of Two by construction: untrusted input, sensitive systems, and the ability to change state. You can’t split that lane without giving up the gateway, so you run it with a clock, a test and a hunt instead.
None of those live in the model or the harness. They live in the layer that runs the fleet: the inventory that knows which gateways exist, the policy that says who may accept a containment outage, the deploy gate that runs the forged-Bearer test, and the record that says when the clock started. That layer is what a multi-agent command center is once you strip the dashboard off it.
LiteLLM will not be the last gateway on KEV. Start the next clock at the advisory.
FAQ
Does LiteLLM 1.84.0 fully fix CVE-2026-59822?
The advisory and NVD name 1.84.0 as the patched release, and its fix PR also closed a ?.well-known query-string bypass. By design it still allows the empty-auth fallback when every target MCP server is configured for OAuth2 passthrough, so for those servers, test that the upstream rejects a forged token.
Is blocking /mcp/ at the reverse proxy enough to mitigate the LiteLLM MCP vulnerability?
It is the advisory’s own workaround, and it covers only traffic that passes through that proxy. A published container port, a node port, a second load balancer or a port-forward goes around it. Treat the block as containment until the upgrade lands, then prove the fix with a forged-Bearer test that returns 401.
Sources
- GitHub advisory GHSA-7488-6r32-c95q — repo advisory Jun 30, 2026; workaround
- NVD: CVE-2026-59822 — Jul 8, 2026; CVSS 3.1 8.2
- CISA Known Exploited Vulnerabilities Catalog — LiteLLM entries May 8, Jun 8, Sep 2
- CISA: BOD 26-04 — cited by the KEV entry
- BerriAI/litellm release v1.84.0 — May 14, 2026
- BerriAI/litellm PR #26463 — the fix, incl. the
?.well-knownbypass - GHSA-v4p8-mg3p-g94g — CVE-2026-42271; Apr 21, 2026
- NVD: CVE-2026-42208 — SQL injection; May 8, 2026
- Wiz: Breaking LiteLLM — discoverer’s timeline; Sep 9, 2026 (vendor)
- Wiz: AI infrastructure honeypot — Aug 27, 2026 (vendor)
