On May 26, 2026, the National Vulnerability Database published CVE-2026-48710, an authentication-bypass weakness in Starlette that the researchers who found it nicknamed "BadHost." Starlette is the lightweight ASGI toolkit that sits underneath FastAPI, so the blast radius is not one product. It is the routing layer beneath a large share of the Python web services, internal APIs, LLM inference servers, and Model Context Protocol gateways running in production right now.
The flaw is simple enough to fit in a single curl command. Starlette rebuilds request.url from the raw HTTP Host header without validating it. Routing, meanwhile, uses the actual request path. Feed the server a malformed Host header and those two values stop agreeing, so any middleware or endpoint that makes a security decision based on request.url.path can be walked straight past. The team at X41 D-Sec rated it CVSS 4.0 7.0; the NVD scored the 3.1 vector at 6.5. Neither number captures the real problem, which is that the bypass defeats a control you thought you had.
The fix shipped in Starlette 1.0.1 on May 21, and the advisory followed on May 22. If you run anything built on FastAPI, this is a this-week problem, not a next-sprint problem. Most teams will not even know they depend on Starlette, because it arrives transitively through FastAPI. That is exactly why this one is worth twenty minutes of your attention.
What BadHost actually does
Starlette reconstructs the request URL roughly as {scheme}://{host_header}{path}. The Host header is attacker-controlled and, prior to 1.0.1, was copied in without being checked against the grammar in RFC 9112 and RFC 3986. The routing engine does not use that reconstructed URL. It dispatches on the raw path from the ASGI scope. So you end up with two different ideas of "what path is this," and an attacker gets to choose when they diverge.
X41's advisory gives the cleanest illustration. Send a request to /foo with a header of Host: example.com/abc?bar= and Starlette builds the URL http://example.com/abc?bar=/foo. Now request.url.path returns /abc, while the router still serves /foo. The classified weakness is CWE-436, interpretation conflict, and it is the same family of bug as classic proxy path confusion. The difference is that here the confusion lives inside your own application framework.
The proof of concept is a one-liner. Against a service that only allows the root path through its auth middleware, this returns a clean 200:
# A normal request to a protected path is blocked
curl -i http://localhost:8000/admin # 401 / 403
# BadHost: a malformed Host header collapses request.url.path to "/"
curl -i -H 'Host: foo?' http://localhost:8000/admin # 200 OK
With Host: foo?, the reconstructed request.url.path evaluates to /. A guard that reasons "only requests to / are public, everything else needs a token" now treats /admin as public. The router, of course, still hands the request to the /admin handler. The forged address fools the guard at the gate while the request walks through a different door.
Why this lands harder on AI and API stacks
This is not a niche library. FastAPI is one of the default ways small teams ship an API in 2026, and it is built on Starlette. The same toolkit underpins a long list of AI infrastructure: self-hosted LLM inference servers, agent orchestration frameworks, retrieval services, and MCP gateways that brokers tool calls for autonomous agents. Many of those services were stood up fast, by one or two developers, and front a path-based access model: /health and /docs are open, /admin or /internal or /tools are "protected."
That pattern is exactly what BadHost defeats. We covered the same structural lesson in our writeup on securing MCP servers and again with the LiteLLM AI gateway pre-auth flaw: the AI plumbing your business now depends on is ordinary web infrastructure with the security maturity of a weekend project. A gateway that routes a prompt to a tool with cloud credentials is a far more interesting target than a marketing brochure, and the auth in front of it is frequently a single middleware function nobody has audited.
Think through what the bypass actually buys an attacker. On an MCP gateway, reaching a "protected" route can mean enumerating the registered tools, triggering a workflow that holds an API key, or pivoting toward the internal services those tools talk to. On an inference server, it can mean hitting an admin route that reconfigures the model or exposes prompts and logs. The vulnerability is rated for limited confidentiality and integrity impact in isolation, but the CVSS number undersells the reality, because in these stacks a single bypassed endpoint is rarely the end of the chain. It is the first link. That is the pattern we keep seeing in SMB AI deployments: one weak control in front of a component that holds real privilege.
Where the exposure usually hides
- Internal FastAPI services on a "trusted" network segment that an attacker reaches after a phishing foothold.
- MCP servers and agent gateways exposed to other services, or to the public internet, with prefix-based authorization.
- Admin or metrics endpoints "protected" by middleware that branches on
request.url.pathrather than the routed path. - Reverse-proxy rules that strip or rewrite based on the application's reported URL.
Find every Starlette you are running
You cannot patch what you cannot see, and the hard part of BadHost is inventory, not remediation. Starlette is almost always a transitive dependency, so grepping for "fastapi" is not enough. Pin down the actual installed version in every service and image:
# In a running container or venv: what version is actually installed?
pip show starlette 2>/dev/null | grep -E '^(Name|Version)'
# Across a fleet, dump it from each environment
pip list --format=freeze | grep -i '^starlette=='
# In a repo, read it out of the lockfiles you actually ship
grep -iE 'starlette' poetry.lock uv.lock requirements*.txt Pipfile.lock 2>/dev/null
Anything reporting a version below 1.0.1 is in scope. X41 traced the defect back to 0.8.3, so do not assume a recent-looking pin is safe. If you already generate a software bill of materials in CI, query it for Starlette across every image at once rather than logging into hosts one by one; if you do not, this is a good reason to start, because the next transitive-dependency CVE will land the same way. Once you have a target list, confirm exploitability against each service with the same two-request test an attacker would run. Treat a 200 on the second line as a confirmed bypass:
# Baseline vs. BadHost probe against a protected path
for h in "normal.example" "foo?"; do
printf '%s -> ' "$h"
curl -s -o /dev/null -w '%{http_code}\n' -H "Host: $h" http://TARGET:8000/admin
done
# normal.example -> 401
# foo? -> 200 <-- path-based guard is bypassable
Fix it, and detect the abuse
The durable fix is the dependency bump. Upgrade Starlette to 1.0.1 or later, which validates the Host header against RFC 9112 and falls back to scope["server"] for malformed values, per the Starlette security advisory and the fix commit. In a FastAPI project, bump the floor explicitly so a resolver does not quietly leave you on a vulnerable build:
# pyproject.toml / requirements
starlette>=1.0.1
# then rebuild and redeploy every image, not just the ones you remembered
While the patch rolls out, fix the code pattern that made you vulnerable in the first place. Security decisions belong on the routed path, not on a value reconstructed from a client-controlled header. Read the raw ASGI scope path instead of request.url.path:
# Vulnerable: trusts request.url.path (rebuilt from the Host header)
class PathGuard(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
if request.url.path.startswith("/admin") and not is_authed(request):
return Response(status_code=403)
return await call_next(request)
# Safer: use the raw scope path that routing actually dispatches on
raw_path = request.scope["path"]
if raw_path.startswith("/admin") and not is_authed(request):
return Response(status_code=403)
Then hunt for exploitation, because the test attackers run looks identical to the test you just ran. A malformed Host header is rare in legitimate traffic, so it is a high-signal indicator. If you front these services with a proxy that logs the host, sweep for headers that do not match a clean hostname grammar:
# Splunk: surface malformed Host headers hitting your ASGI services
index=web (sourcetype=nginx:access OR sourcetype=traefik)
| regex http_host!="^([a-zA-Z0-9._-]+)(:\d{1,5})?$"
| stats count, values(uri_path) as paths by clientip, http_host, status
| sort - count
If your proxy can normalize or reject bad Host values, turn that on as a compensating control: it blunts the attack even on services you have not patched yet. The disclosure was a coordinated, audited effort, with the Open Source Technology Improvement Fund backing the review that found it, which means the technical details are public and the scanners are already reading them.
The move this week
Inventory first, patch second, detect third. Pull the installed Starlette version from every service and image you operate, including the AI gateways and agent runtimes nobody put on the asset list. Upgrade everything below 1.0.1, switch any path-based authorization to the raw scope path, and add a detection for malformed Host headers at the proxy. BadHost is cheap to fix and cheap to exploit, and the gap between those two facts is measured in days. Close it before someone else closes it for you.
Not sure what is exposed in your API and AI stack?
We build open-source tooling for exactly this kind of attack-surface work - finding the services you forgot you run and the auth checks that do not hold. Browse our tools at github.com/redhoundinfosec, or book a session and we will help you inventory your FastAPI, LLM, and MCP footprint and harden the parts that matter.
