Poisoning request.url.path in Starlette / FastAPI
Poisoning request.url.path can lead to bypassing path-based validation
Background
Starlette is a “Starlette is a lightweight ASGI framework/toolkit, which is ideal for building async web services in Python.”. It is heavily used by FastAPI.
In February 2026, I reported this issue to Starlette. At about the same time, X41 D-Sec also reported it. Here is their blog post: https://x41-dsec.de/lab/advisories/x41-2026-002-starlette/.
Starlette issued the GitHub Advisory GHSA-86qp-5c8j-p5mr and CVE-2026-48710.
Vulnerable code
Here are two endpoints that can lead to a path traversal and the oher to an authentication bypass. Can you find the trick?
from starlette.applications import Starlette
from starlette.responses import PlainTextResponse, HTMLResponse
from starlette.routing import Route
from starlette.requests import Request
from starlette.middleware import Middleware
from starlette.middleware.base import BaseHTTPMiddleware
async def home(request):
return PlainTextResponse(f"hello admin, path is: {request.url.path}")
async def test(request):
return PlainTextResponse(f"test handler, path is: {request.url.path}")
async def static(request):
content = open("/app/assets" + request.url.path, "r").read()
return HTMLResponse(content)
class CustomMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
if request.url.path.startswith("/admin"):
return PlainTextResponse("unauthorized")
response = await call_next(request)
return response
middleware = [Middleware(CustomMiddleware)]
routes = [Route("/admin/home", home), Route("/test", test), Route("/static/{file}")]
app = Starlette(routes=routes, middleware=middleware)
# in main.py, uvicorn main:app --reload
Proof-of-Concept
Before we dig into the root cause, here are the PoCs:
curl http://localhost:8000/static/test -H 'Host: localhost:/../../../etc/passwd?'
curl http://localhost:8000/admin/home -H 'Host: localhost/test?'
Root cause
Starlette does not verify the value from the Host header. It uses it to build the request.url object, just as follows.
if host_header is not None:
url = f"{scheme}://{host_header}{path}"
By poisoning the Host, the URL will be equal to http://localhost/test?/admin/home. When request.url.path (or any other properties from request.url is used), the app will return the value from urlsplit(url).someProperty, so request.url.path with the polluted Host will return /test, which bypasses the check if request.url.path.startswith("/admin"). The app will still call the proper route handler though.
@property
def components(self) -> SplitResult:
if not hasattr(self, "_components"):
self._components = urlsplit(self._url)
return self._components
Remediation and best practices
Starlette now ensures the Host header can no longer contain invalid characters via this regex:
_HOST_RE = re.compile(r"^([a-z0-9.-]+|\[[a-f0-9]*:[a-f0-9.:]+\])(?::[0-9]+)?$", re.IGNORECASE)
Although updating Starlette to version 1.0.1 would remediate the issue, here are more recommendations:
- Use
scope["path"]instead ofrequest.url.path. This is what is used to send the request to the proper route handler. - Implement proper authentication/authorization controls on sensitive endpoints.
- Use
StaticFilesfor serving static content.