Your Auth Middleware and Your Router Disagree About What Was Requested. CVE-2026-48710 Is Rated Medium, Was Added to KEV Yesterday, and Sits Under Almost Every Python AI Service You Run.
- Patrick Duggan
- 1 hour ago
- 6 min read
CISA added seven vulnerabilities to the Known Exploited Vulnerabilities catalog on September 2. One of them is rated CVSS 6.5, which is medium, which normally means it waits behind the nines. This one should not.
CVE-2026-48710 is in Starlette, the ASGI toolkit that FastAPI is built on. If you run a Python web service written in the last five years, there is a good chance Starlette is underneath it, two dependencies down, and you have never typed its name.
The bug is that your authorization middleware and your router can be made to disagree about which URL was requested. Both are working correctly. They are just reading different strings.
What actually happens
Starlette exposes the requested URL two ways.
The ASGI server hands the application the raw path off the wire, and the router uses that to decide which endpoint runs. Separately, Starlette offers a convenient request.url property, which it builds by taking the Host header the client sent, gluing the path onto it, and re-parsing the result as a URL.
Prior to version 1.0.1, that Host header was never validated before being used. Not against RFC 9112, which defines what a Host header is allowed to contain, and not against RFC 3986, which defines how a URL gets split into pieces.
So an attacker puts a slash in the Host header.
Consider a request for a protected internal endpoint, sent with a Host header of example.com/public. Starlette concatenates that with the path and re-parses. The parser has no way to know the slash came from the header rather than the path, so the boundary between authority and path lands in the wrong place, and request.url.path now begins with /public. A question mark or a hash character moves the query and fragment boundaries the same way.
Meanwhile the ASGI server is routing on the raw wire path, which never changed. The protected endpoint runs.
Why this hits the people who did it right
Here is the part worth sitting with.
The standard advice for authorization in a Python API is: do not scatter permission checks across your handlers, centralize them in middleware. One place to audit, one place to change, no endpoint accidentally shipped without a guard. That advice is correct and it is what a careful team does.
A middleware written that way asks a question like whether the request path begins with the internal prefix, and if so demands authentication. Written against request.url.path, that check now reads a string the attacker moved. It sees a path starting with /public, concludes no authentication is needed, and passes the request through. The router, reading the untouched wire path, dispatches to the internal endpoint anyway.
The handler runs. The guard was never bypassed in the sense of being defeated — it was consulted honestly and given a forged answer.
Teams that ignored the advice and wrote permission checks inside each individual handler, against the routing path, are less exposed. That is an uncomfortable sentence and it is the correct one. The vulnerability selects for the architecture that was recommended.
The severity number is wrong about what matters
CVSS 6.5. Medium. Partial impact, network vector, low complexity.
CVSS scores a bug's mechanics in isolation. It cannot score what the bug sits underneath. An authentication bypass in a leaf application is a medium. The same authentication bypass in the request layer of the framework that thousands of services inherit is not a medium in any sense a defender cares about, and CISA adding it to KEV on evidence of active exploitation is the field correcting the score with observed behavior.
If your scanner deprioritized this because it is a 6.5, your scanner did what it was told. Tell it something else.
The part that made us pay attention
Look at what else was in the same batch.
CVE-2026-59822, BerriAI LiteLLM, improper authentication. That is LiteLLM's third entry in the catalog in four months, after a SQL injection in May and a command injection in June.
Two authentication bypasses added on the same day, both in the layer that Python AI services are assembled from. LiteLLM is the model gateway sitting in front of CrewAI, DSPy, Microsoft GraphRAG and a long tail of internal deployments. Starlette is what FastAPI is built on, and FastAPI is what a very large share of inference endpoints, retrieval services and Model Context Protocol servers are written in.
We named LiteLLM as a compromised component on March 24 of this year, after the TeamPCP actor pushed malicious versions to PyPI, and we wrote up the Trivy to LiteLLM to Telnyx chain on April 1. We want to be precise about what that does and does not entitle us to say. We flagged the component five months before this listing. We did not call this CVE, and none of the three LiteLLM entries is the same bug as the others. A package compromise is not an authentication bypass. The honest claim is that the component kept earning attention, not that we predicted this specific flaw.
What is fair to say is the pattern. The AI infrastructure layer is now generating known-exploited vulnerabilities at a rate the AI security conversation is not tracking, because that conversation is largely about model behavior. Nobody is jailbreaking anything here. Somebody put a slash in a header.
How big is the exposed surface, measured rather than asserted
Everyone writing about this will tell you FastAPI is popular. Here is what our own index says, which is a different kind of claim.
We continuously crawl and snapshot the Model Context Protocol server registry. As of this morning that corpus holds 5,802,194 snapshot records, of which 1,829,173 are current, latest-version servers — 1,812,074 active and 17,099 deprecated.
The Host-header bug can only reach a server that speaks HTTP. A server running over stdio, on the same machine as its client, is not in the blast radius at all. So the number that matters is how many of those servers are exposed over HTTP, and on a random sample of 1,000 current servers, 880 of them — 88.0 percent — publish at least one HTTP endpoint. The protocol split is 880 streamable-http to 109 server-sent-events, with overlap.
Extrapolated, that is on the order of 1.6 million current MCP servers reachable over HTTP. Not all are Python and not all are Starlette. But that is the population inside which an ASGI request-layer authentication bypass is even a question, and it is the first time we have been able to put a measured number on it.
Now the part we cannot tell you, and why. We cannot give you the framework split. Our registry records carry a language field and a transport field, and while auditing this we found both are populated on exactly zero of the 5.8 million records — declared in the index schema, never filled by the collector. A query for Python servers returns a confident zero that means "we never collected this," not "there are none." That is our bug, found while writing this, and it is the same false-zero shape we spend most of our time hunting in other people's telemetry.
What we can offer is a floor from free text. Searching current server descriptions: FastAPI is named in 108, uvicorn in 117, LiteLLM in 350. Starlette and ASGI appear in zero, which surprises nobody — almost no one names the toolkit two layers below the thing they wrote. Those are mentions, not dependencies, and the real counts are certainly far higher. Treat 108 as the number of authors who bothered to say it, not the number who use it.
The honest summary: 1.6 million HTTP-reachable servers is the outer bound, the Python share of that is unknown to us today, and we would rather publish the bound with its uncertainty attached than a confident number we cannot support.
What to do
Upgrade Starlette to 1.0.1 or later. The fix validates the Host header against the RFC grammars when constructing request.url and falls back to the value the ASGI server reports when the header is malformed.
You probably do not depend on Starlette directly. Check what your FastAPI version pins, and check your lockfile rather than your requirements file, because the requirements file is what you asked for and the lockfile is what you got.
Then go read your middleware. Anywhere a security decision is made from request.url, change it to read the routing path out of the ASGI scope instead. That is the value the router itself uses, it is not reconstructed from anything the client controls, and it cannot be moved by a header. Fixing the framework closes this bug. Reading the same string your router reads closes the whole class of them.
And if you run a model gateway, an inference endpoint, or an MCP server, assume you are in scope for both of yesterday's authentication bypasses until you have checked, because the odds are good that FastAPI is holding your front door and something like LiteLLM is holding the one behind it.
Was this useful? Rate this post — the widget is at the bottom of the page, and we read every response.
Every indicator in this post is in the feed. Free.
1.58M+ IOCs, STIX 2.1 / TAXII, 88% novel vs ThreatFox, exploited-CVE leads ahead of CISA. No credit card — a free API key in 30 seconds, and you can audit every claim above against the live endpoints.
Was this useful? Thirty seconds, no cookies, no tracking, no third parties, your address hashed and never stored. If the box below does not load, the same question lives at https://analytics.dugganusa.com/nps.html?post=your-auth-middleware-and-your-router-disagree-about-what-was-requested-cve-2026-48710-is-rated-medi




Comments