CVE-2026-48710 is a Host header parsing flaw in Starlette that can turn a fragile middleware pattern into an authorization bypass. The issue is also known as BadHost. The affected condition is specific: Starlette before 1.0.1 could reconstruct request.url from an attacker-controlled Anfitrião header in a way that made request.url.path differ from the path actually used for routing. If application code used that reconstructed URL path for security decisions, a malformed Host header could make protected routes look unprotected to middleware while the router still dispatched the request to the sensitive endpoint.(NVD)
That distinction matters. CVE-2026-48710 is not a universal remote code execution bug in Starlette. It is not proof that every FastAPI or Starlette application is exploitable. It is a framework-level inconsistency that becomes dangerous when applications build access control, allowlists, blocklists, tenant routing, SSRF guards, or admin protections on top of request.url ou request.url.path. Starlette 1.0.1 fixed the issue by ignoring malformed Host values when constructing request.url, using Host grammar aligned with RFC 9112 and RFC 3986, and falling back to scope["server"] when the Host value is invalid.(GitHub)
The official severity picture is still worth reading carefully. NVD lists the record as awaiting enrichment, while the GitHub advisory reports a CVSS 3.1 base score of 6.5, Medium. X41, the research team behind the public advisory, rated the issue High under CVSS 4.0, and the BadHost site uses stronger language because the bug can become severe in real deployments that protect high-value API paths with middleware. The right operational conclusion is not to argue over one adjective. Patch Starlette, inspect path-based security code, and verify whether malformed Host headers can reach your application.(NVD)
The facts security teams need first
| Item | What is known |
|---|---|
| CVE | CVE-2026-48710 |
| Common name | BadHost |
| Affected component | Starlette, the ASGI framework used directly and through frameworks such as FastAPI |
| Versões afetadas | Starlette versions before 1.0.1 are the primary affected range described by the advisory and OSV record |
| Fixed version | Starlette 1.0.1 |
| Classe de vulnerabilidade | Inconsistent interpretation of HTTP request data, mapped by NVD to CWE-444 |
| Typical vulnerable pattern | Middleware or application code using request.url ou request.url.path to make security-sensitive decisions |
| Typical impact | Path-based authorization bypass, security middleware bypass, and downstream impact depending on what the protected route can do |
| Typical non-impact case | Applications that enforce authorization at route or endpoint level and do not rely on reconstructed URL strings for access control |
| Main fix | Upgrade Starlette and remove fragile path-string authorization logic |
| Important compensating control | A fronting proxy or gateway that rejects malformed Host headers before they reach the ASGI app, provided forwarded Host headers are not attacker-controlled |
GitHub’s advisory states the core issue plainly: Starlette did not validate the HTTP Anfitrião header before using it to reconstruct request.url. An invalid Host containing reserved URI delimiters could cause the parsed URL path to differ from the path actually requested. The router would still use the raw request path, but middleware reading request.url.path could see a different path.(GitHub)
Starlette’s release notes for 1.0.1 list the fix as ignoring malformed Host headers when constructing request.url. The corresponding commit adds Host validation and includes tests for invalid Host values containing characters such as /, ?, #, @, backslash, and spaces. Those characters are important because they can alter how a URI parser divides authority, path, query, and fragment components.(starlette.io)
Why this bug exists at the HTTP boundary
To understand CVE-2026-48710, start with the difference between the HTTP request target and the Host header.
A normal HTTP/1.1 origin-form request looks like this:
GET /admin/settings?tab=users HTTP/1.1
Host: api.example.com
The request target is /admin/settings?tab=users. The Host header is api.example.com. They are separate pieces of data. RFC 9112 requires clients to send Host in HTTP/1.1 requests and requires servers to reject requests that are missing Host, contain more than one Host field, or contain an invalid Host field value. The same specification explains that the origin-form request target carries the path and optional query, while Host supplies authority information.(Editor de RFC)
RFC 3986 defines the authority component as the part following // and ending at the next /, ?, #, or the end of the URI. That means those delimiter characters are not harmless inside Host-like authority text. If a framework concatenates a Host header with a path and then gives the result to a URI parser, an attacker-controlled delimiter inside Host can change where the parser thinks the authority ends and where the path or query begins.(Editor de RFC)
ASGI adds another important layer. In the ASGI HTTP scope, caminho is a decoded path value excluding the query string, raw_path may preserve the original path bytes, query_string stores the query string, and cabeçalhos carries request headers. A framework can expose both low-level scope values and higher-level convenience objects such as request.url. The security problem appears when two layers derive different meanings from the same request.(ASGI Documentation)
In Starlette before 1.0.1, the router and request.url did not always agree when the Host header was malformed. Routing relied on the actual request path. URL reconstruction used the Host header and path together. When Host was valid, that was normal and useful. When Host contained URI delimiters, the reconstructed URL could parse differently from the actual request path.(GitHub)
The exploit is path confusion, not router control

The vulnerable mental model looks like this:
if request.url.path.startswith("/admin"):
require_admin_user()
That code assumes request.url.path is the same path the Starlette router used to select the endpoint. CVE-2026-48710 breaks that assumption when the Host header is malformed and reaches Starlette.
The GitHub advisory gives a simple example. A request may be routed as /foo, while the reconstructed URL parses as a different path because a malformed Host header moves the real path into another URI component. The X41 advisory shows the same class of mismatch and demonstrates that middleware can make an authorization decision based on the reconstructed path while the router still dispatches the actual requested path.(GitHub)
A simplified vulnerable Starlette middleware might look like this:
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse
class AdminPathGate(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
if request.url.path.startswith("/admin"):
token = request.headers.get("authorization")
if token != "Bearer expected-admin-token":
return JSONResponse({"detail": "forbidden"}, status_code=403)
return await call_next(request)
The code is not malicious. It is also easy to understand why a developer might write it. Middleware sits before the endpoint, the admin area lives under /admine request.url.path looks like the obvious property to inspect. But in vulnerable Starlette versions, request.url.path was not safe as a security boundary if a malformed Host header could influence URL reconstruction.
The safer design is not just to replace one string with another. Long-term access control should be attached to routes, endpoints, dependencies, or authorization policies that are not derived from a reconstructed URL string. For FastAPI, endpoint-level dependencies are a common way to bind authorization to the actual route being executed:
from fastapi import Depends, FastAPI, Header, HTTPException
app = FastAPI()
async def require_admin(authorization: str | None = Header(default=None)) -> None:
if authorization != "Bearer expected-admin-token":
raise HTTPException(status_code=403, detail="forbidden")
@app.get("/admin/settings", dependencies=[Depends(require_admin)])
async def admin_settings():
return {"status": "ok"}
That pattern does not ask a reconstructed URL object whether the route is sensitive. The protected route declares its own requirement. Standard FastAPI security dependencies and route-level enforcement are not the same as a custom middleware that performs path-prefix authorization using request.url.path. FastAPI is built on Starlette, so dependency versions still matter, but the vulnerable application pattern is narrower than “any FastAPI app.”(FastAPI)
As a temporary reduction of this specific risk, Starlette code that absolutely must inspect the request path should prefer ASGI scope values over request.url.path:
path = request.scope.get("path", "")
if path.startswith("/admin"):
require_admin_user()
That reduces exposure to this Host-based URL reconstruction bug, but it is not a complete authorization architecture. Path-string checks still have other problems: trailing slashes, normalization, percent encoding, route aliases, mounted sub-applications, case behavior, reverse proxy rewrites, and future refactors. Starlette’s maintainer explicitly warned that authorization should not be based on request path, host, or query string when better route-bound options exist.(Marcelo Trylesinski)
A minimal request-level example
Consider this request:
GET /admin/panel HTTP/1.1
Host: example.com/?x=
Connection: close
The actual request target is /admin/panel. A router that uses the request path can dispatch the admin endpoint. But if the application reconstructs a URL by concatenating scheme, Host, and path, then parses it as a URI, the ? inside Host can make the parser interpret later data as query text rather than as the path the application expected.
That is the essence of BadHost. The attacker does not need to change the route. The attacker needs to change what security middleware thinks the route is.
X41’s public proof of concept shows a route protected by middleware that returns 403 when the path begins with /admin. A normal request receives 403. A request using a malformed Host header reaches the same protected endpoint but receives 200 because the middleware reads a different request.url.path.(x41-dsec.de)
For authorized internal testing, a small raw HTTP probe can help confirm whether malformed Host headers reach your service and whether the response differs from the control case. Keep probes narrow, run them only against systems you own or are explicitly authorized to test, and do not turn this into internet-wide scanning.
#!/usr/bin/env python3
import socket
import ssl
import sys
from urllib.parse import urlparse
def send_raw(url: str, host_header: str) -> bytes:
parsed = urlparse(url)
scheme = parsed.scheme or "https"
port = parsed.port or (443 if scheme == "https" else 80)
hostname = parsed.hostname
path = parsed.path or "/"
if parsed.query:
path += "?" + parsed.query
request = (
f"GET {path} HTTP/1.1\r\n"
f"Host: {host_header}\r\n"
"User-Agent: authorized-badhost-check/1.0\r\n"
"Connection: close\r\n"
"\r\n"
).encode("ascii", "replace")
with socket.create_connection((hostname, port), timeout=5) as sock:
if scheme == "https":
ctx = ssl.create_default_context()
with ctx.wrap_socket(sock, server_hostname=hostname) as tls:
tls.sendall(request)
return tls.recv(4096)
sock.sendall(request)
return sock.recv(4096)
if __name__ == "__main__":
if len(sys.argv) != 3:
print("usage: python badhost_check.py https://api.example.com/admin/panel 'api.example.com/?x='")
raise SystemExit(2)
response = send_raw(sys.argv[1], sys.argv[2])
print(response.decode("iso-8859-1", "replace"))
A meaningful test is comparative. Send a valid Host request first. Confirm the expected authentication behavior. Then send a malformed Host request and confirm whether the service rejects it at the edge, rejects it in the application, or changes behavior. A single 200 response is not enough to prove impact unless you also show the protected route, authentication expectation, and why the response demonstrates a bypass.
Who is actually exposed

CVE-2026-48710 exposure is best judged with a decision table rather than a yes-or-no dependency check.
| Condition | Risk meaning | What to verify |
|---|---|---|
| Starlette is older than 1.0.1 | The framework behavior may be vulnerable | Check the runtime package version, not only the source tree |
| The app uses FastAPI | FastAPI depends on Starlette, but exploitability depends on application code and dependency version | Inspect resolved dependencies and custom middleware |
Middleware uses request.url.path for auth | High-risk pattern | Search code for path-prefix authorization and access gates |
Middleware uses request.url for allowlists, tenant routing, SSRF guards, or callback checks | Potentially high impact | Determine whether the decision protects sensitive behavior |
| A reverse proxy rejects malformed Host before forwarding | Strong mitigating control | Test whether invalid Host reaches the ASGI server |
The app trusts X-Forwarded-Host from untrusted clients | Possible bypass of proxy-level assumptions | Confirm trusted proxy configuration and header stripping |
| Authorization is route-bound or endpoint-bound | Lower risk from this bug class | Verify no earlier middleware creates conflicting allow/deny logic |
| Sensitive AI, admin, or internal control endpoints exist | Impact can rise even if the base CVSS is medium | Map protected actions reachable behind path gates |
The most dangerous pattern is a direct ASGI deployment or a permissive reverse proxy in front of a Starlette or FastAPI application that uses custom middleware for path-level access control. That includes internal services if the internal network is not a trusted boundary. Modern incidents repeatedly show that “internal only” often means “reachable from CI runners, staging networks, compromised workloads, developer VPNs, service mesh peers, or SSRF primitives.”
The least concerning pattern is a patched Starlette deployment, with strict edge Host validation, route-bound authorization, and no security decisions based on reconstructed URL strings. Even then, it is worth adding regression tests because the same design mistake can reappear in future middleware.
Why FastAPI and AI infrastructure deserve special attention
FastAPI is compatible with Starlette and is built on top of it; FastAPI’s FastAPI class is a subclass of Starlette. That does not mean every FastAPI service is exploitable, but it does mean Starlette dependency state matters for FastAPI deployments.(FastAPI)
The BadHost site highlights AI-adjacent infrastructure because many LLM APIs, inference gateways, agent servers, MCP services, and internal developer tools are written with FastAPI or Starlette. That observation should be handled with precision. CVE-2026-48710 is not an LLM vulnerability. It does not compromise a model because the model is large or because the endpoint uses AI. The concern is that AI infrastructure often exposes powerful actions through Web APIs: model management, file access, tool invocation, workflow execution, admin dashboards, token management, connector configuration, and internal proxying. If those actions sit behind fragile path-based middleware, the business impact of a bypass can be much higher than the framework bug sounds in isolation.(CVE-2026-48710 – Nemesis – BadHost)
MCP servers and agent gateways make this especially interesting for defenders. An agent control plane may have endpoints that are boring in a normal CRUD app but dangerous in an automation context: run a tool, fetch a URL, read a connector, execute a workflow, upload a plugin, rotate credentials, or change a model provider key. A path confusion flaw near that control plane can turn a medium framework issue into a serious operational incident.
The practical response is to inventory AI-facing ASGI services before lower-value applications. Prioritize anything that has one or more of these traits:
| Service trait | Why it increases priority |
|---|---|
| Exposes admin or operator paths | Path-based middleware is common around admin prefixes |
| Executes tools or workflows | A bypass may lead to actions beyond data exposure |
| Manages API keys, model keys, or connector tokens | Confidentiality impact can be high |
| Runs in a flat internal network | Internal attackers or SSRF may reach it |
| Uses custom auth middleware | Custom path checks are more likely than in standard dependency-based auth |
| Trusts forwarded headers | Host assumptions may be weaker than expected |
| Is deployed directly with Uvicorn or another ASGI server | Edge rejection of malformed Host may be absent |
The safest sentence for engineering teams is simple: do not assume “we use FastAPI” means vulnerable, and do not assume “we are behind a proxy” means safe. Check the version, the middleware, the edge behavior, and the authorization model.
What Starlette changed in 1.0.1
Starlette 1.0.1 changed how malformed Host values affect URL construction. The fix ignores invalid Host headers when constructing request.url and falls back to server information from the ASGI scope. The patch adds validation to prevent Host values containing characters that can make urlsplit produce a path different from scope["path"]. The tests added in the patch cover values such as foo/?x=, foo/#, foo/bar, user@foo, foo\bare foo bar.(GitHub)
That patch is the correct foundation because it fixes the framework-level inconsistency. But the application-level lesson remains: security code should not depend on a reconstructed URL string when more authoritative route or scope data is available. Frameworks can harden dangerous edge cases, but they cannot rescue every fragile authorization pattern.
A dependency check can identify candidates for urgent review:
python - <<'PY'
import importlib.metadata as md
from packaging.version import Version
try:
version = md.version("starlette")
except md.PackageNotFoundError:
print("starlette is not installed in this Python environment")
raise SystemExit(0)
print(f"starlette {version}")
if Version(version) >= Version("1.0.1"):
print("patched for CVE-2026-48710 according to the upstream fixed version")
else:
print("needs review: upgrade Starlette and inspect path-based security code")
PY
For package managers, the upgrade may look like one of these, depending on your project:
python -m pip install --upgrade "starlette>=1.0.1"
python -m pip install --upgrade "fastapi" "starlette>=1.0.1"
poetry add "starlette>=1.0.1"
uv add "starlette>=1.0.1"
Treat those commands as examples, not universal deployment instructions. FastAPI and other frameworks may pin compatible Starlette ranges. Your lockfile, test suite, and runtime environment decide what actually ships. The OSV record identifies Starlette 1.0.1 as the fixed PyPI version, while distribution packages may have backported fixes without matching PyPI’s exact version number.(osv.dev)
Static review, find the dangerous patterns
A version check tells you whether the framework bug exists. It does not tell you whether your application had exploitable security logic. Code review is the next step.
Start with a broad search:
rg -n "request\.url|request\.url\.path|str\(request\.url\)|URL\(scope=" .
rg -n "BaseHTTPMiddleware|@app\.middleware\(['\"]http['\"]\)|Middleware\(" .
rg -n "startswith\(['\"]/|in \[['\"]/|path_prefix|allowlist|denylist|blacklist|whitelist" .
rg -n "x-forwarded-host|forwarded|trustedhost|proxy_headers|root_path" .
Look for security decisions, not just references. Logging request.url is usually not exploitable by itself. Building a redirect from request.url can be risky, but the BadHost authorization bypass pattern is strongest when a branch controls access:
if request.url.path.startswith("/internal"):
enforce_internal_auth()
if request.url.path not in PUBLIC_PATHS:
require_login()
if request.url.path.startswith("/api/admin") and not user.is_admin:
return forbidden()
if "/webhook/" in request.url.path:
skip_csrf_check()
Each of those patterns deserves attention because a mismatch between routed path and reconstructed URL path can change security behavior. The same review should include tenant routing, callback validation, SSRF prevention, and proxy dispatch if those decisions depend on the reconstructed URL.
A more structured review table helps triage findings:
| Code pattern | Risk level | Por que é importante | Preferred change |
|---|---|---|---|
request.url.path.startswith("/admin") before auth | Alta | Can skip auth if reconstructed path differs | Use route-bound or endpoint-bound authorization |
request.url.path in PUBLIC_PATHS to skip login | Alta | Sensitive route may appear public | Attach public/private status to route definitions |
str(request.url) used for security redirects | Médio | May create host or path confusion | Use configured canonical origins and explicit redirect targets |
request.url.hostname used for tenant trust | Medium to high | Host is client-controlled unless strictly validated | Use verified tenant mapping after edge validation |
request.scope["path"] used for temporary gate | Lower for BadHost | Not affected by reconstructed Host URL mismatch | Still prefer route-bound policy |
request.url used only in logs | Baixa | May pollute logs but does not decide access | Sanitize logs and keep edge rejection |
If you use Semgrep, a first-pass local rule can flag the riskiest shape. This is intentionally broad and should be tuned for your codebase:
rules:
- id: starlette-request-url-path-security-check
languages: [python]
severity: WARNING
message: "Review security logic based on request.url.path. In Starlette before 1.0.1 this may be affected by CVE-2026-48710, and path-string authorization is fragile even after patching."
patterns:
- pattern-either:
- pattern: |
if $REQ.url.path.startswith($PREFIX):
...
- pattern: |
if $REQ.url.path in $PATHS:
...
- pattern: |
if $REQ.url.path == $PATH:
...
- pattern: |
if $PATH in $REQ.url.path:
...
The rule will produce false positives. That is acceptable for triage. The goal is to find places where a reviewer can answer three concrete questions:
- Does this code make a security decision?
- Can an attacker influence the Host or forwarded Host seen by the app?
- Does the protected endpoint have meaningful confidentiality, integrity, or availability impact?
Behavioral validation in a safe environment
A good CVE-2026-48710 validation has three parts: dependency evidence, code evidence, and behavioral evidence.
Dependency evidence answers whether a vulnerable Starlette version is present. Code evidence answers whether the application uses a vulnerable pattern. Behavioral evidence answers whether the malformed Host actually changes security behavior in the deployed environment.
A clean test plan looks like this:
| Etapa | Test | Expected safe result | Risky result |
|---|---|---|---|
| 1 | Request protected path with normal Host and no auth | 401 or 403 | 200 or sensitive response |
| 2 | Request same path with malformed Host and no auth | 400 at edge, or same 401/403 | 200 or different access behavior |
| 3 | Request same path with valid auth | Expected authorized response | Confirms route is real |
| 4 | Repeat through public edge and internal service address | Edge should reject or app should behave consistently | Internal path differs from edge behavior |
| 5 | Check application logs | Rejected malformed Host or consistent auth logging | Route handler executed while auth middleware saw different path |
Avoid testing random hosts on the internet. This bug is easy to probe, and large-scale probing can look like exploitation. In a bug bounty program, stay within scope and include a minimal number of requests.
If you run AI-assisted validation, keep it evidence-based. Penligent’s AI pentesting material emphasizes checking exposure, affected version ranges, reachable conditions, patch state, and safe behavioral evidence rather than relying only on banners or version strings. That discipline fits CVE-2026-48710 well: a useful result should connect dependency state, middleware code, edge behavior, and reproducible request evidence. Penligent’s platform is built around guided AI-assisted penetration testing, tool orchestration, verification, and reporting, which can be useful for keeping this kind of retest consistent across many services without turning the finding into an unreviewed scanner alert.(Penligente)
Reverse proxies help, but do not end the review
The GitHub advisory notes that proxies can mitigate the issue if they reject or normalize malformed Host headers before forwarding, and if the application does not trust attacker-controlled X-Forwarded-Host or similar headers elsewhere. That caveat is important. A reverse proxy is a strong control only when its behavior is known, tested, and paired with correct forwarded-header trust boundaries.(GitHub)
A safe edge posture usually includes these practices:
| Controle | Finalidade | Falha comum |
|---|---|---|
| Host allowlist | Only expected hostnames reach the app | Wildcard hosts or default server blocks forward unexpected traffic |
| Invalid Host rejection | Malformed values never reach Starlette | Proxy forwards raw Host to backend |
| Header stripping | Client-supplied X-Forwarded-Host is removed | Backend trusts attacker-controlled forwarded headers |
| Canonical origin config | App uses configured public origin for redirects and links | App derives trust from request Host |
| Backend isolation | ASGI server is not directly reachable | Internal service port exposed to VPN, pod network, or staging users |
| Regression tests | Edge behavior remains stable across config changes | Proxy upgrade or ingress change reopens malformed Host path |
For Nginx-style deployments, prefer an explicit server name and avoid forwarding arbitrary Host values to the application unless the application truly needs multi-tenant host behavior. A simplified pattern is:
server {
listen 443 ssl;
server_name api.example.com;
location / {
proxy_set_header Host api.example.com;
proxy_set_header X-Forwarded-Host api.example.com;
proxy_set_header X-Forwarded-Proto https;
proxy_pass http://starlette_backend;
}
}
That example is intentionally strict. Some applications need the original Host for tenant routing. If so, validate it against known tenants at the edge and again in application code. Do not accept arbitrary Host values and later treat them as trusted security context.
For HAProxy-style deployments, the control often appears as a deny rule for malformed Host values and an allowlist for known domains:
acl host_has_bad_delimiter req.hdr(Host) -m reg "[/?#@\\ ]"
http-request deny deny_status 400 if host_has_bad_delimiter
acl allowed_host req.hdr(Host) -i api.example.com
http-request deny deny_status 421 unless allowed_host
The exact syntax should be tested against your proxy version and configuration style. The goal is not to copy a snippet blindly. The goal is to guarantee that Host values containing URI delimiters cannot reach the ASGI application.
Forwarded headers can reopen the door
Many ASGI deployments sit behind a proxy that rewrites connection information. That is normal. The danger appears when the backend trusts forwarded headers from any client instead of only from a known proxy.
If a backend or middleware uses X-Forwarded-Host to reconstruct URLs, then an edge that rejects malformed Anfitrião may still be bypassed if the attacker can send a malformed X-Forwarded-Host and the application treats it as authoritative. The GitHub advisory explicitly limits proxy mitigation to cases where the app does not trust attacker-controlled forwarded Host values.(GitHub)
Review these settings and code paths:
rg -n "X-Forwarded-Host|Forwarded|TrustedHost|ProxyHeaders|proxy_headers|forwarded_allow_ips|root_path" .
Then answer:
| Pergunta | Safe answer |
|---|---|
Who can set X-Forwarded-Host? | Only the trusted edge proxy after stripping client-supplied copies |
| Does the app trust proxy headers from all IPs? | Não |
| Is the ASGI server directly reachable? | Não |
| Does the proxy normalize or reject malformed Host? | Yes, verified with tests |
| Does application auth depend on reconstructed URL path? | Não |
Forwarded headers are not automatically bad. They are necessary in many deployments. They become dangerous when they are treated as authenticated facts while still being client-controlled input.
Logging and detection
CVE-2026-48710 leaves several useful signals. Some appear at the edge, some in application logs, and some only after you add instrumentation.
A malformed Host header is the most obvious indicator. Host values containing /, ?, #, @, backslash, or spaces should be rare in normal production traffic. They are strong candidates for rejection and alerting. Starlette’s patch tests specifically include these categories because they can alter URL parsing.(GitHub)
Useful detection signals include:
| Sinal | Where to look | Por que é importante | Caveat |
|---|---|---|---|
Host contains /, ?, #, @, backslash, or space | Edge proxy logs, WAF logs, app logs | Direct probe indicator | Some logs may normalize or omit invalid headers |
| Protected path returns 2xx without auth | App access logs, auth logs | Possible bypass | Must compare with expected auth behavior |
App logs show routed path and request.url.path differ | Custom instrumentation | Direct evidence of desync | Requires logging both values |
| 400 or 421 for malformed Host | Edge proxy logs | Healthy rejection | Confirm backend never receives the request |
Untrusted X-Forwarded-Host with delimiters | Proxy and app logs | Possible alternate injection path | Header may be stripped before logging |
Sudden probes across /admin, /internal, /metrics, /debug | Edge logs | Recon or exploit attempt | Common scanner noise, correlate with Host anomalies |
A temporary diagnostic middleware in a staging environment can reveal mismatches:
from starlette.middleware.base import BaseHTTPMiddleware
class PathDiagnosticMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
scope_path = request.scope.get("path")
url_path = request.url.path
if scope_path != url_path:
# Use your structured logger here. Do not log secrets.
print({
"event": "path_mismatch",
"scope_path": scope_path,
"url_path": url_path,
"host": request.headers.get("host"),
"x_forwarded_host": request.headers.get("x-forwarded-host"),
})
return await call_next(request)
Do not leave noisy diagnostic logging in production without review. Host headers may contain attacker-controlled content, and logs can become an injection surface for downstream tools. For production, prefer structured logging, truncation, sanitization, and alerting at the edge.
Dependency reality, PyPI, lockfiles, containers, and distributions
Python dependency state is often messier than a single pip show starlette.
A service may use Starlette directly. It may use FastAPI, which depends on Starlette. It may vendor part of a framework. It may run from a container image built weeks before the lockfile changed. It may use a Linux distribution package with a backported fix. It may also run multiple Python environments across workers, background jobs, and admin servers.
A reliable inventory should include:
python -m pip show starlette fastapi
python -m pip freeze | rg "starlette|fastapi"
python - <<'PY'
import importlib.metadata as md
for pkg in ["starlette", "fastapi", "uvicorn"]:
try:
print(pkg, md.version(pkg))
except md.PackageNotFoundError:
print(pkg, "not installed")
PY
For Poetry:
poetry show starlette fastapi
poetry lock --no-update
For uv:
uv pip list | rg "starlette|fastapi"
uv tree | rg "starlette|fastapi"
For containers:
docker run --rm your-image:tag python -m pip show starlette fastapi
docker run --rm your-image:tag python - <<'PY'
import importlib.metadata as md
print("starlette", md.version("starlette"))
PY
For SBOM workflows, search the generated CycloneDX or SPDX artifact for Starlette:
rg -n '"name": "starlette"|"name":"starlette"|PackageName: starlette|starlette' sbom.*
Distribution packages need special care. Debian’s tracker, for example, distinguishes vulnerable and fixed package states across releases, including security backports. A Debian package may not show the same version number as PyPI’s fixed release while still carrying a fix, or it may remain vulnerable in a release line until the security update is applied.(security-tracker.debian.org)
The operational rule is simple: verify the runtime artifact. The lockfile is evidence, not proof. The container image, host package, and running Python environment are what matter.
Why the same bug gets different severity labels
CVE-2026-48710 is a good example of why CVSS is useful but not sufficient.
GitHub’s advisory scores the issue as CVSS 3.1 6.5 Medium. NVD displays the record as awaiting enrichment and maps the weakness to CWE-444. X41 rates the advisory High using CVSS 4.0. Public BadHost material uses stronger language because the issue can affect middleware in applications that protect sensitive routes.(NVD)
Those views can coexist because they answer different questions.
| Ver | What it captures | What it may understate |
|---|---|---|
| Framework base score | The generic vulnerability in Starlette | Business impact of the protected endpoint |
| Research advisory score | Likely exploitability and common deployment patterns | Whether a given app has the vulnerable middleware pattern |
| Public impact framing | Worst-case composition across popular stacks | The fact that many apps are not exploitable |
| Internal risk rating | Your actual endpoint, auth model, exposure, and data | Hard to compare across organizations |
A medium base score can still require urgent action if the vulnerable path controls admin functions, secrets, agent tools, file access, deployment actions, or internal proxying. A high public advisory can still be non-exploitable in a particular environment if Starlette is patched, the edge rejects malformed Host, and authorization is endpoint-bound.
Good security teams avoid both extremes. They do not dismiss the issue because the base score is medium. They also do not declare a breach because a dependency is present. They test the actual chain.
Related CVE, Apache HTTP Server CVE-2023-25690
CVE-2026-48710 belongs to a broader family of bugs where different components interpret request data differently. A useful comparison is CVE-2023-25690 in Apache HTTP Server.
Apache describes CVE-2023-25690 as an HTTP request smuggling issue affecting some mod_proxy configurations in Apache HTTP Server 2.4.0 through 2.4.55. The vulnerable condition involved configurations where RewriteRule ou ProxyPassMatch used a non-specific pattern and reinserted user-controlled request-target data into the proxied request. Apache states that this could lead to request splitting or smuggling, bypass access controls, proxy unintended URLs, and poison caches. The fix shipped in Apache HTTP Server 2.4.56.(httpd.apache.org)
The two CVEs are not the same bug. CVE-2023-25690 is an Apache proxy configuration issue. CVE-2026-48710 is a Starlette URL reconstruction issue triggered by malformed Host values and dangerous application assumptions. But they rhyme at the architectural level:
| Aspecto | CVE-2026-48710 | CVE-2023-25690 |
|---|---|---|
| Main component | Starlette | Apache HTTP Server with specific proxy rewrite configurations |
| Core theme | Reconstructed URL path differs from routed path | Proxy rewrite can reinterpret user-controlled request-target data |
| Typical impact | Middleware authorization bypass or security logic bypass | Request smuggling, access control bypass, unintended proxying, cache poisoning |
| Main fix | Upgrade Starlette and avoid request.url.path security gates | Upgrade Apache and avoid unsafe proxy rewrite patterns |
| Broader lesson | Do not base trust on reconstructed request strings | Do not reinsert ambiguous user-controlled request-target data into downstream requests |
The shared lesson is stronger than either CVE alone: the path a security check sees must be the same path the application, proxy, cache, and upstream service will act on. Any gap between those interpretations can become an attack surface.
Hardening application authorization
The best fix for CVE-2026-48710 is to upgrade Starlette. The best fix for the design class is to remove path-string authorization from generic middleware.
A stronger FastAPI pattern binds authorization to the endpoint:
from fastapi import Depends, FastAPI, HTTPException, Request
app = FastAPI()
async def current_user(request: Request) -> dict:
token = request.headers.get("authorization")
if token != "Bearer expected-token":
raise HTTPException(status_code=401, detail="missing or invalid token")
return {"sub": "user-123", "role": "user"}
async def require_admin(user: dict = Depends(current_user)) -> None:
if user.get("role") != "admin":
raise HTTPException(status_code=403, detail="admin required")
@app.get("/admin/audit-log", dependencies=[Depends(require_admin)])
async def audit_log():
return {"events": []}
@app.get("/health")
async def health():
return {"ok": True}
The public route is public because it has no admin dependency. The admin route is protected because its route declaration says so. There is no global middleware trying to infer sensitivity from a reconstructed string.
For Starlette, route-level structure can accomplish the same idea. The exact implementation depends on your authentication system, but the policy should attach to route definitions, endpoint metadata, or explicit permission checks rather than prefix guesses.
If you must keep middleware during migration, make it conservative:
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse
SENSITIVE_PREFIXES = ("/admin", "/internal", "/ops")
class TemporaryScopePathGate(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
path = request.scope.get("path", "")
if path.startswith(SENSITIVE_PREFIXES):
token = request.headers.get("authorization")
if token != "Bearer expected-admin-token":
return JSONResponse({"detail": "forbidden"}, status_code=403)
return await call_next(request)
Then add tests that prove the policy holds for malformed Host values:
from starlette.testclient import TestClient
def test_admin_rejects_malformed_host_without_auth(app):
client = TestClient(app)
response = client.get(
"/admin/audit-log",
headers={"host": "example.com/?x="},
)
assert response.status_code in {400, 401, 403}
The exact expected status depends on whether the framework, proxy simulation, or app rejects the request first. The important assertion is that the protected endpoint does not return a successful unauthenticated response.
Hardening URL and origin handling beyond this CVE
BadHost is about request.url.path, but the same input trust problem often appears in other places.
Applications frequently use request-derived URLs for redirects, password reset links, callback validation, tenant selection, CORS decisions, webhook routing, and SSRF controls. CVE-2026-48710 should trigger a broader audit of how your application treats Host and URL-derived values.
Review code like this:
reset_url = str(request.url_for("reset_password", token=token))
if request.url.hostname.endswith(".trusted.example"):
allow_tenant_action()
if request.url.path.startswith("/webhook/"):
skip_csrf()
return RedirectResponse(str(request.url.replace(path="/login")))
Some of that code may be safe in context. Some may be risky. The question is whether the value is being used for display, routing, policy, or trust.
A hardened approach uses configured canonical origins:
from urllib.parse import urljoin
PUBLIC_ORIGIN = "https://app.example.com"
def build_public_url(path: str) -> str:
if not path.startswith("/"):
raise ValueError("path must be absolute")
return urljoin(PUBLIC_ORIGIN, path)
For tenant routing, use a validated mapping:
ALLOWED_TENANT_HOSTS = {
"tenant-a.example.com": "tenant_a",
"tenant-b.example.com": "tenant_b",
}
def tenant_from_host(host: str) -> str:
try:
return ALLOWED_TENANT_HOSTS[host.lower()]
except KeyError:
raise PermissionError("unknown tenant host")
For security-sensitive behavior, prefer explicit configuration and verified context over request-derived reconstruction. Host is a routing hint from the client until your edge and application validate it.
Patch and retest plan for production teams
A practical remediation plan should avoid two common mistakes. The first mistake is patching the package but leaving vulnerable middleware in place. The second is rewriting middleware but leaving a vulnerable Starlette version exposed to future code paths.
Use a two-track plan.
| Track | Ação | Verificação |
|---|---|---|
| Framework | Upgrade Starlette to 1.0.1 or a downstream fixed package | Runtime version check and dependency lock review |
| Application | Remove request.url.path security gates | Code review and regression tests |
| Edge | Reject malformed Host and restrict allowed hosts | Raw request tests through public ingress |
| Forwarded headers | Strip and reset X-Forwarded-Host at trusted proxy | Confirm backend cannot receive client-supplied forwarded Host |
| Monitoramento | Alert on malformed Host probes | Edge logs and application logs |
| Reporting | Attach evidence to ticket | Dependency, code, request, response, and fix proof |
A deployment checklist can look like this:
[ ] Identify every service running Starlette directly or through FastAPI.
[ ] Confirm the runtime Starlette version inside the deployed artifact.
[ ] Upgrade to Starlette 1.0.1 or a vendor package with the backported fix.
[ ] Search for request.url.path and request.url in security-sensitive code.
[ ] Replace path-string authorization with route-bound or endpoint-bound checks.
[ ] Add regression tests for malformed Host values against sensitive paths.
[ ] Confirm edge proxy rejects malformed Host headers.
[ ] Confirm client-supplied X-Forwarded-Host is stripped or ignored.
[ ] Retest public ingress and internal service paths.
[ ] Keep evidence with the change request or incident ticket.
For bug bounty hunters and red teamers, a high-quality report should include the same structure. Do not stop at “Host header can contain a question mark.” Show the affected Starlette version, the vulnerable middleware or behavior, the control request, the malformed Host request, the difference in authorization outcome, and the business impact of the reached endpoint. If the target is behind a proxy, document whether the proxy forwarded the malformed Host or whether an alternate forwarded Host path was involved.
Common mistakes during remediation
The most common remediation mistake is treating this as a simple dependency alert. A dependency alert is valuable, but it does not prove exploitability and it does not remove fragile authorization logic. If the same code later runs behind a different proxy, or a future framework bug creates another mismatch, the design flaw remains.
The second mistake is trusting the proxy without testing it. Many teams assume Nginx, Apache, an ingress controller, a cloud load balancer, or an API gateway rejects malformed Host values. Some do. Some configurations do not. Some reject at the public edge but allow direct access to the backend service. Some reject Anfitrião but pass X-Forwarded-Host. Test the path your traffic actually takes.
The third mistake is using a denylist of bad paths. For example:
if not request.url.path.startswith("/public"):
require_login()
This can fail in surprising ways even beyond CVE-2026-48710. It is safer to make sensitive endpoints explicitly require authorization than to infer public and private state from path strings.
The fourth mistake is ignoring internal services. BadHost is easy to think of as a public internet issue, but internal AI gateways, admin APIs, and staging services often have weaker edge controls and stronger privileges. Internal does not mean harmless.
The fifth mistake is overclaiming. A scanner result that says “Starlette < 1.0.1” is a lead, not a final vulnerability report. A malformed Host response difference is suspicious, but the meaningful question is whether a protected action became reachable without the required security condition.
PERGUNTAS FREQUENTES
Is every FastAPI app vulnerable to CVE-2026-48710?
- No. FastAPI is built on Starlette, so FastAPI deployments should check their resolved Starlette version, but exploitability depends on application behavior.
- The highest-risk pattern is custom middleware that uses
request.url.pathourequest.urlto decide whether authentication, authorization, CSRF checks, tenant rules, or other security controls apply. - FastAPI endpoint dependencies and route-bound security checks are not the same as fragile path-prefix middleware.
- Patch Starlette anyway. A non-exploitable app today can become exploitable later if new middleware or deployment paths are added.
What exactly does Starlette 1.0.1 change?
- Starlette 1.0.1 ignores malformed Host headers when constructing
request.url. - The patch validates Host grammar so characters that can alter URL parsing, such as
/,?,#,@, backslash, or spaces, do not create a reconstructed URL path different fromscope["path"]. - If Host is malformed, Starlette falls back to server information from the ASGI scope rather than trusting the invalid Host value.
- The fix addresses the framework inconsistency, but applications should still avoid authorization based on reconstructed URL strings.(GitHub)
Can a reverse proxy fully mitigate BadHost?
- A reverse proxy can be a strong mitigation if it rejects malformed Host headers before they reach the ASGI application.
- The mitigation is incomplete if the backend is directly reachable, if the proxy forwards arbitrary Host values, or if the app trusts client-supplied
X-Forwarded-Host. - You should test the public ingress and any internal backend path separately.
- Even with a strong proxy, upgrade Starlette and remove fragile path-based security logic. Defense in depth is better than relying on one layer.
Why do different sources score the same issue differently?
- GitHub’s advisory gives a CVSS 3.1 score of 6.5 Medium, while X41 rates the issue High under CVSS 4.0, and public BadHost material uses stronger impact language.
- The difference comes from context. The base framework issue is conditional, but the impact can rise sharply when the bypass protects admin functions, AI agent actions, internal proxy routes, secrets, or RCE-capable operations.
- Internal severity should be based on your environment: affected version, reachable malformed Host, vulnerable middleware, and the value of the protected endpoint.
- Do not dismiss the issue only because one source says Medium, and do not claim compromise only because a dependency is present.
Should I search only for request.url.path?
- No.
request.url.pathis the most obvious pattern, but related security decisions may userequest.url,str(request.url),request.url.hostname, URL replacement helpers, tenant routing logic, or forwarded Host middleware. - Search for
BaseHTTPMiddleware,@app.middleware("http"), allowlist and denylist logic, public path lists, admin path prefixes, and forwarded header handling. - Separate harmless logging from security decisions. A log statement is not the same as an authorization gate.
- Review any code that derives trust, access, tenant identity, redirect targets, or SSRF boundaries from request-derived URL values.
What should a good bug bounty report include?
- The affected Starlette version or dependency evidence from the target.
- The specific endpoint or behavior that should require authentication or another security condition.
- A control request with a valid Host showing the expected denial.
- A malformed Host request showing a different result.
- Evidence that the protected action, data, or route was actually reached.
- A clear explanation of whether the edge proxy forwarded the malformed Host or whether forwarded headers were involved.
- A remediation recommendation: upgrade Starlette, fix path-based middleware, and validate Host at the edge.
Does this matter for AI agents, MCP servers, and LLM APIs?
- Yes, if those systems use Starlette or FastAPI and expose powerful actions behind Web/API paths.
- The vulnerability is not model-specific. The model is not the root cause.
- The risk comes from the control plane around AI systems: tool execution, file access, connector actions, token management, workflow execution, admin APIs, and internal model gateways.
- Prioritize AI infrastructure that has custom middleware, direct ASGI exposure, weak internal network boundaries, or sensitive admin endpoints.
Julgamento final
CVE-2026-48710 is a small parsing bug with a large design lesson. Security controls should not depend on a URL string reconstructed from client-controlled Host data when routing, proxying, and middleware may interpret request components differently. The fix starts with Starlette 1.0.1, but it should not end there.
Patch the framework. Remove request.url.path authorization gates. Validate Host at the edge. Strip untrusted forwarded headers. Add regression tests for malformed Host values against sensitive paths. Treat AI and agent infrastructure as high-priority if it exposes powerful actions through ASGI services. The most durable remediation is not just blocking one malformed header; it is making sure the security boundary is tied to the route and policy actually being executed.

