NEWS
BadHost: Discovery and Details
Overview
Starlette < 1.0.1 derives request.url from the Host header without sanitization,
letting attackers forge a request.url.path that bypasses path-based auth middleware.
Thousands of FastAPI and Starlette applications were affected, including vLLM, LiteLLM, MCP servers, and AI agent frameworks.
vLLM Authentication Middleware
The BadHost vulnerability (CVE-2026-48710) was discovered by X41 during our source code audit of the vLLM project, organized by OSTIF.
In this post we describe how we found the issue, explain the vulnerability in detail, and go over its impact, the disclosure, and publication process. The issue was found during human code review.
What initially caught our eye in vLLM’s AuthenticationMiddleware were the following lines:
from starlette.datastructures import URL
# ...
url_path = URL(scope=scope).path.removeprefix(root_path)
if url_path.startswith("/v1") and not self.verify_token(headers):
response = JSONResponse(content={"error": "Unauthorized"}, status_code=401)
return response(scope, receive, send)
return self.app(scope, receive, send)
This authentication check only applies to paths starting with /v1 and otherwise allows the request to pass through.
Such a path-based approach of enforcing authentication is not best practice, but it’s easy to implement and may be an obvious choice to developers. It is fairly common and widely used. Even outside of web applications, many reverse proxy configurations enforce directives in a similar, path-based way.
vLLM has this middleware because its OpenAI API (/v1) endpoints can be protected with an API key, while other requests such as /version should pass through without authentication.
We started wondering whether it would be possible to trick the middleware into a state where it sees a url_path not starting with /v1 while the application would still route the request to the relevant /v1 endpoint without providing an API key.
The first attempts were to use http://example.com//v1/chat/completions and http://example.com/foo/../v1/chat/completions, but those failed because there is no path normalization, i.e., those paths are understood literally and only lead to a 404 response.
Understanding ASGI HTTP Requests
Before we dig deeper, let’s first understand how requests are parsed and passed through.
vLLM is based on the FastAPI Python framework which is designed to build HTTP APIs and is generally a widely popular choice. FastAPI itself is based on Starlette which implements ASGI (the Asynchronous Server Gateway Interface) on the receiving end. ASGI is a standard to exchange parsed HTTP requests and responses between HTTP web servers and Python programs. With a Python application that speaks ASGI (e.g., through Starlette), you can put any ASGI-capable web server in front and thereby expose it to the web. Notably, neither FastAPI nor Starlette directly parse raw HTTP messages, as that’s the job of the ASGI web server. So when an HTTP request is sent over the Internet, the receiving web server parses it, transforms it into an ASGI object, calls the ASGI interface of Starlette and FastAPI from where it may flow through middlewares and eventually reaches the actual application’s endpoint.
Exploitation
After understanding this, we looked deeper into how Starlette reconstructs the URL based on the request scope (the ASGI representation of the HTTP request).
Now it’s important to understand that when sending a request such as http://example.com/foo, this URL is not available as such to the receiving web server. Instead, the URL is first split by the sending application into scheme (http), host (example.com), and path (/foo). What is actually sent in a request looks like this:
GET /foo HTTP/1.1
Host: example.com
If the receiving application wants to learn what the requested URL was, it needs to do the inverse. Starlette does this by concatenating the URL string as {scheme}://{host_header}{path}, where {host_header} is the value of the Host: request header and {path} is what was submitted as the second word in the first request line. So far, this is nothing out of the ordinary. The problem is that the value of the Host: header is entirely user-controlled, and can be manipulated to include parts of the path. So a GET /foo request sent with Host: example.com/bar would be concatenated to http://example.com/bar/foo, where the resulting path starts with /bar. Starlette does not use this URL for routing decisions and instead relies on the raw path from the request line (/foo). So the routing decision is not affected by changes to the Host header. This means we can introduce a discrepancy between the path the router uses and the URL (and its .path) that developers of middlewares and endpoints see.
This allowed us to trigger an authentication bypass in vLLM:
GET /v1/models HTTP/1.1
Host: localhost:8000/foo
The URL the middleware sees would be http://localhost:8000/foo/v1/models. The url_path based on it starts with /foo and bypasses the authentication requirement because it does not start with /v1, while the router still sees /v1/models from the request line and sends us to the right endpoint.
Finding the Root Cause
We quickly realized that this issue is likely not specific to vLLM. But where does the issue come from, and what would be the correct behavior? To find out, we consulted the good old HTTP RFCs and found an answer in RFC 9112 Section 3.2:
A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message […] that contains […] a Host header field with an invalid field value.
The valid values are defined in RFC 9110 Section 7.2 and RFC 3986 Section 3.2.2 and do not allow for delimiters such as / or ?.
Okay, clearly the server should reject such a request. We started testing all the ASGI servers documented on the ASGI website at the time, namely daphne, hypercorn, uvicorn, granian, and NGINX Unit. To our surprise, none of them rejected this kind of request (except NGINX Unit which allowed ? but not /).
But whose job is it, actually, to reject this invalid request?
The flow chart from above depicted how many components can be involved in the handling of HTTP requests within FastAPI applications. And while the HTTP RFCs define the server behavior, they don’t clearly map to the real-world complexity of what a server is. To the RFCs, it’s all a single black box which in reality consists of several pieces of software maintained by different parties. The boundaries between server and application are blurred, and this makes it particularly difficult to assess where the responsibility lies.
X41 concluded that, in this particular case, ideally the ASGI servers would reject the request because they’re the only ones actually parsing HTTP messages. There is no legitimate interest for any of the later components to process requests with invalid headers. Additionally, it would prevent similar issues from occurring in other ASGI frameworks or other downstream users.
This would however require that all the available ASGI servers uniformly introduce patches that reject such requests. The second-best thing would then be Starlette validating the Host header and rejecting invalid requests.
Disclosure
After a lot of internal discussion on how and with which stakeholders to coordinate a disclosure, the issue was then disclosed to the vLLM project, the ASGI framework Starlette, the ASGI servers daphne, hypercorn, uvicorn, granian, NGINX Unit, and the Rust HTTP library hyper (which granian depends on). Additionally, OSTIF was included to help with coordination. X41 aimed to streamline a common solution with the open source maintainers and prevent an uncoordinated disclosure by one of the involved parties (e.g., releasing a patch and thereby disclosing the vulnerability before others had a patch ready).
Coordination initially posed a challenge because some projects preferred reports via email while others preferred GitHub Security Advisories. The latter can only be opened against individual projects and do not allow researchers to invite additional stakeholders. Thankfully, the Starlette maintainer stepped in and invited other relevant stakeholders to the Starlette advisory thread, so a joint, private, discussion could take place there.
While the coordination was ongoing, X41 attempted to find affected products and estimate the overall impact of the issue.
A Common Problem
Why is this issue so common at all?
Path-based authentication is generally a rather widely used anti-pattern. It may seem like an obvious solution at first but comes with many pitfalls, with this instance being just one of many.
To access the request path in a Starlette or FastAPI project, developers can use request.url.path or request.scope.get("path"). The latter does not suffer from the BadHost issue, but it’s less obvious from the naming that “scope” would include the path. It may also feel a bit more cumbersome to type and uses the rather low-level ASGI scope object, so overall the request.url.path option is very popular.
Impact Assessment
We wrote Semgrep rules to find uses of this by scanning source code. These rules were later converted to CodeQL queries, and both have been released.
However, FastAPI and Starlette are massively popular with millions of downloads per day. To find good targets for scanning, we analyzed dependency graphs from GitHub and PyPI, and then tried ordering them by metrics such as GitHub stars or downloads. To help sift through the massive amounts of repositories, an LLM harness was utilized to automatically clone and set up projects, find uses of request.url.path (and derivatives), and confirm if the project is vulnerable with a PoC.
It quickly became clear that many tools built around LLMs and MCP servers were affected. In the next step, X41 teamed up with Persistent Security’s Nemesis team and Bintech to conduct large-scale Internet scans based on HTTP requests in addition to our source code scans. While several projects unrelated to LLMs were found to be affected, the overall impact from the MCP servers was the largest, as MCP servers are being slapped onto absolutely everything. We’ve found access to document management systems, identity verification, medical data, and much more. Details can be found in the Nemesis blog post.
Coordination
While the RFCs clearly say that the HTTP server must reject invalid requests, none of the ASGI servers wanted to implement this check. They simply pass through the information and are not affected by the vulnerability. The same could be said about Starlette, which does use the header but is also, in itself, not affected by the vulnerability. It only materializes when using a certain kind of path-based authentication pattern in an application. But developers also can’t really expect the request.url.path to be misleading. As such, there was no distinct point where the vulnerability lay and no obvious place to patch it. However, it is infeasible to identify and notify every application using the vulnerable pattern. A coordinated disclosure would also become impossible with such an approach. To effectively tackle the issue, a patch release was required in addition to a CVE or other identifier to allow maintainers and audit tools to become aware of the issue.
In the end, Starlette agreed to release a patch.
Notably, Python’s standard urllib does not make it straightforward to address the issue. It is very lax about the URL components and does not perform validation, as noted in the library’s documentation. Compare this, for example, to Go’s net/url or Rust’s url crate, which would throw errors when constructing a URL from invalid components. An ecosystem that encourages strict validation and secure defaults could have prevented this vulnerability.
X41 had concerns about the ease of exploitation, high number of affected projects, and severity of the impact. On Thursday, May 21, 2026, X41 suggested posting a heads-up public service announcement (PSA) that informs about an upcoming security patch release and its scheduled release time. This common practice allows users to reserve time and be ready to apply the patch as soon as it becomes available. A PSA of the Drupal project was given as an example. We further suggested releasing the patch on Tuesday because Monday would be a public holiday in many countries (Whit Monday, U.S. Memorial Day, and others).
Unfortunately, the PSA suggestion seems to have been misinterpreted as posting details about the vulnerability without a patch, and the idea was rejected. The Starlette patch and advisory were published on GitHub around midnight (CEST) between Thursday and Friday, May 22, 2026. Following that, X41 published its advisory the next morning. A CVE was not yet assigned at this time. We then got PYSEC-2026-161 assigned from the Python Advisory Database. This allows mapping the vulnerability to affected pip releases and identification in SBOMs via OSV compatible tools such as pip-audit, uv audit, Dependabot, and others.
Although we noticed pull requests being opened (mostly by automated security scanners) over the weekend, the updates hadn’t gained much traction. The publication before the weekend without a prior announcement was unfortunate for projects maintained professionally as a day job. This made it a long weekend of unpatched systems that attackers could potentially abuse. As there was also little public reporting about the vulnerability, and we noticed that many popular projects and deployments were still affected, X41 decided to raise awareness about the issue.

We named the vulnerability “BadHost” and worked on a website with more details, FAQ, and a public scanner.
The badhost.org website was published on Tuesday morning, May 26.
OSTIF also published an announcement on the same day.
Applications behind popular reverse proxies or CDNs (such as nginx, Apache httpd, or Cloudflare) were initially thought not to be exploitable. But X41 later realized that some deployments accept the X-Forwarded-Host header and forward it as Host, which bypasses any Host header validation that may be in place.
Malicious actors have chained BadHost with other vulnerabilities. For example, the authenticated RCE of CVE-2026-42271 could be turned into an unauthenticated RCE. CISA added BadHost to its Known Exploited Vulnerabilities (KEV) Catalog in September 2026.
Timeline
All dates in CEST.
2026-01-27 Issue identified in vLLM source code audit
2026-02-04 PoC created and maintainers contacted individually
2026-02-05 Maintainers invited to single thread, coordination ongoing
2026-03-01 Initial Starlette patch developed, coordination ongoing
2026-05-21 X41 proposes PSA, and informs about upcoming public holiday
2026-05-21 Starlette patch published
2026-05-22 Advisories published
2026-05-22 PYSEC-2026-161 assigned
2026-05-22 CVE-2026-48710 assigned
2026-05-26 BadHost website published
2026-05-26 OSTIF announcement published
2026-05-27 CVE entry published by the NVD
2026-06-05 Vulnerability added to GitHub Advisory Database
2026-09-02 Vulnerability added to CISA KEV Catalog
2026-10-07 This post and vLLM audit results published