Back to posts
AINews

LiteLLM accepted a fabricated bearer token on its MCP endpoint, and Starlette let a Host header rewrite `request.url.path`

Two packages that sit underneath most Python AI services carry the same CISA due date, 16 September. Neither flaw is a missing authentication check. LiteLLM's check ran, failed, and substituted an empty auth object. Starlette's middleware checked a path the router never used. Both fixes have been on PyPI since May, upgrading FastAPI does not move Starlette, and the credential worth rotating is not the one you would reach for first.

Two rows from CISA's Known Exploited Vulnerabilities catalog, both added on 2 September, both with a due date of today:

CVEPackageFlawFixed in
CVE-2026-59822BerriAI LiteLLMImproper authentication on the MCP Streamable HTTP endpoint (CVSS 8.8)1.84.0
CVE-2026-48710Kludex StarletteHTTP request/response smuggling via the Host header (CVSS 6.5)1.0.1

The due date binds federal civilian agencies under BOD 26-04. For everyone else it carries a different piece of information: CISA lists a vulnerability in this catalog when there is evidence it is being exploited, not when it is merely severe. LiteLLM is an API gateway with 58,868 stars on GitHub; Starlette is the ASGI layer under FastAPI. If you run a Python service in front of a model, one of these is probably in your lockfile.

Both patches shipped in May. LiteLLM 1.84.0 went out on 14 May, 125 days ago. Starlette 1.0.1 went out on 21 May, 118 days ago. The reason this is still a live catalog entry is that the fix being available and the fix being installed are different facts.

The check, and what each answer means

pip show litellm starlette | grep -E '^(Name|Version)'

Three outcomes. litellm below 1.84.0 means the MCP endpoint is affected, but only if you turned it on, which is the next check. starlette at 1.0.0 or below means affected. Anything above those lines is fine; current releases are LiteLLM 1.101.0 and Starlette 1.6.0.

The LiteLLM MCP gateway is opt-in. It requires database storage, so the exposure condition is specific:

grep -n 'store_model_in_db' config.yaml; echo "STORE_MODEL_IN_DB=$STORE_MODEL_IN_DB"

If neither is set, you never had MCP routes to expose and the LiteLLM half of this does not apply to you. If either is set and you are below 1.84.0, the endpoint was reachable.

What got through LiteLLM

The advisory describes a fallback, not an omission. LiteLLM's MCP auth handler supports OAuth2 passthrough so that upstream MCP servers can do their own authentication. When LiteLLM key validation failed, that path did not reject the request. It replaced the failed validation with an empty UserAPIKeyAuth() object and carried on.

The practical result, in the advisory's words, is that an unauthenticated caller could "list and call configured MCP tools" and reach the services connected behind them. A request with an Authorization header containing a value the attacker made up was indistinguishable, downstream of that fallback, from a request bearing a real key.

The fix is PR #26463, titled "tighten public-route detection and OAuth2 fallback gating", and it landed alongside eleven other authentication and MCP hardening changes in the same release. If you cannot upgrade in the next hour, the advisory's interim mitigation is to disable MCP routes or block /mcp/ at your reverse proxy or API gateway, which is a one-line change in most deployments and is worth doing while the upgrade goes through review.

What got through Starlette

Starlette reconstructs request.url from the Host header. Before 1.0.1 it did not validate that header first, so characters that carry meaning in a URL survived into the reconstruction and moved the boundaries between path, query, and fragment.

Send this:

Host: example.com/abc?bar=

against a request routed to /foo, and request.url.path reads /abc. Routing is unaffected, because the router uses the raw ASGI value in scope["path"], and that still says /foo. The request goes where it was always going. Only the code that asked request.url a question got a different answer.

That gap matters in exactly one place: middleware that authorizes on request.url.path. A check written as "reject anything under /admin" sees /abc, allows the request, and the router then dispatches /admin/... as normal. The advisory notes this can be chained with CVE-2026-42271.

The grep that finds your exposure:

grep -rn 'request\.url\.path' --include='*.py' .

Hits inside middleware or dependency functions that make an allow-or-deny decision are the ones to read. Hits in logging and metrics are noise. Where a security decision is being made, scope["path"] is the value that matches what the router will do, and it was never affected by this.

Starlette 1.0.1 contains one change, PR #3279: "Ignore malformed Host header when constructing request.url."

Two things the upgrade does not do

Upgrading FastAPI does not move Starlette. FastAPI 0.141.1 declares starlette>=0.46.0, a lower bound with no ceiling. A lockfile pinning starlette==1.0.0 satisfies that constraint perfectly, so pip install -U fastapi resolves, reports success, and leaves you exactly where you were. A clean install today resolves to 1.6.0 and a locked environment does not. This is why the check above reads the installed Starlette version rather than the FastAPI version.

Patching LiteLLM does not revoke what already went through it. This is the part neither advisory is written to tell you. Rotating your LiteLLM virtual keys is the reflex, and it is the wrong target: the fabricated token was never a real key, so there is nothing about it to invalidate. What ran were the configured MCP tools, and a tool call that reached a connected service authenticated with that server's own stored credentials. Those are the credentials the bypass exercised, and they are untouched by both the upgrade and a key rotation. If your MCP servers hold tokens for a ticket tracker, a database, or a cloud account, those are the rotation list, and your proxy access logs for /mcp/ are where you find out whether anyone asked.

The shape both of these share

We have written before about an unauthenticated endpoint on an agent platform, where the Langflow code-validation route simply had no authentication on it. These two are a different class, and the difference is worth carrying forward.

In both cases the authentication check ran. LiteLLM's ran, returned a failure, and the surrounding code treated failure as a reason to continue with a blank identity rather than a reason to stop. Starlette's ran, returned a pass, and was correct about a value that was not the one the router used. Neither shows up as a missing decorator or an unprotected route in a code review. Both look, from the outside, like a service with authentication.

Two questions generalize out of this, and they apply to any auth path you own. What does this check do when it fails, and is that behavior written down anywhere or is it a try/except that someone added to stop an error appearing in the logs? And is the value being checked the same value the framework will act on, or a second, more convenient representation of it that something upstream can influence?

If you are standing up MCP servers behind a gateway, the audit in our stateless MCP migration checklist covers the inventory side of the same surface: which servers exist, what each one is allowed to reach, and who can ask.

Get the next post when it ships

One email on Sunday with the new post and a short list of what shipped that week — new guides, tool updates, and a couple of links worth reading.