CVE
CVE-2026-48710
CWE
CWE-436
Affected Surface
- Starlette `<= 1.0.0`
- FastAPI and other Python services that bundle affected Starlette releases
- Middleware, dependencies, and endpoints that make security decisions from `request.url` or `request.url.path` instead of the raw ASGI `scope["path"]`
- Direct-to-ASGI deployments, or proxy frontends that forward malformed `Host` values instead of rejecting them
CVE-2026-48710 is not a routing bug in the narrow sense. Starlette still routes the request to the correct endpoint. The problem is that user code often does not read the router’s internal path. It reads request.url.path, and in affected versions that value could be attacker-controlled.
CISA added the bug to KEV on 2 September 2026, months after the original advisory landed. That timing is the important new signal. It means the flaw is no longer only a code-review curiosity about malformed headers and URL parsing. It is now an actively exploited boundary failure in one of the Python web stack’s most common foundations.
The vulnerable reconstruction step
Starlette rebuilt request.url from the incoming scheme, Host header, and raw path. In affected versions, the relevant code path effectively did this:
if host_header is not None:
url = f"{scheme}://{host_header}{path}"
elif server is None:
url = path
That looks harmless until the Host value stops being a normal host.
The advisory and X41 write-up both use the same style of example:
GET /foo HTTP/1.1
Host: example.com/abc?bar=
Starlette then reconstructs:
http://example.com/abc?bar=/foo
When that string is parsed back into a URL object, the path becomes /abc, even though the real HTTP request path that reached the server and router was /foo.
That produces the dangerous split:
router dispatches on /foo
middleware sees request.url.path == /abc
Any middleware or endpoint code that gates access on request.url.path can now reason about a different request from the one Starlette actually dispatched.
The exploit is small because the trust mistake is small
X41’s proof of concept shows why this bug matters operationally:
class AuthMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
if request.url.path == "" or request.url.path == "/":
return await call_next(request)
return PlainTextResponse("Forbidden\n", status_code=403)
The route table still contains an /admin endpoint, but the middleware only allows /:
curl -i -H 'Host: foo' localhost:8000/admin # 403
curl -i -H 'Host: foo?' localhost:8000/admin # 200
That is enough to turn one parser inconsistency into real authorization bypass.
The AppSec lesson is broader than Starlette itself. A large amount of Python application logic assumes request.url.path is a faithful mirror of the request line. In affected versions it was not.
Why this reaches FastAPI and AI infrastructure
Starlette is not an edge package with a tiny blast radius. It is the URL and request layer under FastAPI and under a wide swath of internal tooling, admin surfaces, LLM gateways, eval dashboards, and MCP-style control planes. OSTIF and X41 both call that out directly, and the risk model is easy to follow:
- proxy or middleware trusts
request.url.path - router still dispatches the real path
- attacker sends malformed
Host - security check and executed endpoint disagree about which path was requested
That can lead to:
- path-prefix auth bypass
- bypass of “internal only” route checks
- SSRF when a gated endpoint performs outbound fetches
- remote code execution in downstream applications when the wrongly exposed endpoint can run tools, plugins, shell-like actions, or model-loading paths
Not every affected Starlette service becomes RCE. The bug is one step earlier than that. It lets the attacker get to routes your middleware believed it had blocked. The impact depends on what those routes do.
CISA’s KEV entry also notes that the flaw can be chained with CVE-2026-42271. That matters because CVE-2026-42271 is LiteLLM’s authenticated command-execution issue on MCP stdio test endpoints. Put differently: a framework-level path-confusion bug becomes much worse when the application behind it already has sensitive control-plane routes.
The fix is simple and that is useful
The Starlette 1.0.1 patch is short enough to read in one screen.
It adds an allowlist regex for valid host syntax:
_HOST_RE = re.compile(
r"^([a-z0-9.-]+|\[[a-f0-9]*:[a-f0-9.:]+\])(?::[0-9]+)?$",
re.IGNORECASE,
)
and changes the reconstruction guard to:
if host_header is not None and _HOST_RE.fullmatch(host_header):
url = f"{scheme}://{host_header}{path}"
If the header is malformed, Starlette now falls back to the server tuple instead of trusting the hostile host string during URL reconstruction.
The regression tests show the exact payload classes the maintainers wanted to kill:
foo/?x=foo/#foo/baruser@foofoo\\barfoo bar
That list is worth reading because it makes the bug concrete. The issue was not “we forgot an RFC.” The issue was “characters that move authority, path, query, or userinfo boundaries were accepted inside a field that should have stayed a host.”
The versioning problem downstream
One awkward part of this story is that “upgrade Starlette” is not always a one-line response for downstream projects. Applications that pin FastAPI or vendor their own dependency set may not be able to land on starlette==1.0.1 without a larger framework upgrade.
That is already visible in downstream issue traffic. Some LiteLLM releases, for example, pinned FastAPI in a way that transitively resolved vulnerable Starlette versions. That does not change the remediation target, but it does change the work involved. Teams should verify the actual resolved Starlette version inside built images and virtual environments rather than assuming a top-level requirements file tells the whole truth.
What to check in your code
First, confirm the resolved package version:
python3 - <<'PY'
import starlette
print(starlette.__version__)
PY
Then look for security-sensitive path checks that read reconstructed URL state:
rg -n 'request\.url(\.path|\b)|url\.path' app src . 2>/dev/null
High-risk patterns include middleware, dependencies, decorators, and auth helpers that make access decisions before endpoint logic runs.
If you find code like:
if request.url.path.startswith("/admin"):
review whether it should instead be using:
request.scope["path"]
That does not replace patching the framework. It removes the specific trust error from your own code too.
Response guidance
- Upgrade to
starlette >= 1.0.1, or to a framework release that actually resolves that fixed version. - Rebuild containers, virtualenvs, Lambda layers, and bundled artifacts. This is a dependency-resolution issue, not only a source-code issue.
- Audit middleware and auth helpers for uses of
request.urlandrequest.url.path. - Put a proxy or load balancer in front of direct ASGI services and verify that it rejects malformed
Hostvalues instead of forwarding them. - For exposed AI gateways, internal admin panels, and tool-execution services, treat this as a route-exposure problem and review the routes that were assumed to be hidden behind path-based guards.
The important change this week is not that Starlette gained a new bug. It is that an old one crossed the KEV threshold. For Python application stacks that still trust request.url.path at security boundaries, that should be enough to move this from backlog cleanup to active remediation.
From research to remediation
Check whether this pattern exists in your codebase
Turn this research into a remediation workflow. Scan dependencies and package manifests for similar supply-chain risk, then prioritize fixes with reachability context.