Starlette: Unvalidated request path concatenated into authority poisons request.url.hostname
Description
Unvalidated request path concatenated into URL authority allows attacker-controlled hostname in request.url, misleading security decisions.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Unvalidated request path concatenated into URL authority allows attacker-controlled hostname in request.url, misleading security decisions.
Vulnerability
In affected versions of Starlette, the HTTP request path is not validated before being used to reconstruct request.url. The URL is rebuilt by concatenating {scheme}://{host}{path} and re-parsing the result. When the path does not begin with / (e.g., @google.com), the authority boundary shifts during re-parsing, causing request.url.hostname and request.url.netloc to become attacker-controlled. This requires an ASGI server that forwards a request-target lacking a leading / into scope["path"]. The exact version range is not disclosed in the available references [1][2].
Exploitation
An attacker sends an HTTP request with a path that does not start with /, such as GET @google.com HTTP/1.1 with Host: localhost. No authentication or special privileges are required; only network access to the server is needed. The server reconstructs the URL as http://localhost@google.com, and re-parsing yields username = "localhost" and hostname = "google.com". The path @google.com does not match any registered route, so routing typically returns a 404 response, but the poisoned request.url.hostname is available to any code that inspects the URL before or after routing [1][2].
Impact
Applications that use request.url, request.url.netloc, or request.url.hostname for security-sensitive decisions—such as host-based authorization, redirect/callback base URL construction, SSRF target validation, cache key generation, or audit logging—may be misled into trusting an attacker-supplied host. The impact is limited compared to similar Host header poisoning (GHSA-86qp-5c8j-p5mr) because the malformed path does not match any route, so endpoint handlers are not executed. However, middleware, error handlers, or logging code that reads request.url.hostname before routing could still be affected [1][2].
Mitigation
The vendor has released a fix for this vulnerability; consult the advisory for the specific patched version. As a workaround, ensure that a fronting proxy or load balancer rejects HTTP request-targets that do not begin with /. No other mitigations are documented in the available references [1][2].
AI Insight generated on Jun 15, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2Patches
1167b5850e809Build `request.url` from structured components (#3326)
2 files changed · +41 −10
starlette/datastructures.py+10 −10 modified@@ -47,19 +47,19 @@ def __init__( break if host_header is not None and _HOST_RE.fullmatch(host_header): - url = f"{scheme}://{host_header}{path}" - elif server is None: - url = path - else: + netloc = host_header + elif server is not None: host, port = server default_port = {"http": 80, "https": 443, "ws": 80, "wss": 443}[scheme] - if port == default_port: - url = f"{scheme}://{host}{path}" - else: - url = f"{scheme}://{host}:{port}{path}" + netloc = host if port == default_port else f"{host}:{port}" + else: + netloc = None - if query_string: - url += "?" + query_string.decode() + query = query_string.decode() + if netloc is not None: + url = SplitResult(scheme=scheme, netloc=netloc, path=path, query=query, fragment="").geturl() + else: + url = f"{path}?{query}" if query else path elif components: assert not url, 'Cannot set both "url" and "**components".' url = URL("").replace(**components).components.geturl()
tests/test_datastructures.py+31 −0 modified@@ -126,6 +126,10 @@ def test_url_from_scope() -> None: assert u == "/path/to/somewhere?abc=123" assert repr(u) == "URL('/path/to/somewhere?abc=123')" + u = URL(scope={"path": "/path/to/somewhere", "query_string": b"", "headers": []}) + assert u == "/path/to/somewhere" + assert repr(u) == "URL('/path/to/somewhere')" + u = URL( scope={ "scheme": "https", @@ -192,6 +196,33 @@ def test_url_from_scope_with_invalid_host(host: bytes) -> None: assert u.netloc == "example.com" +@pytest.mark.parametrize( + "path, expected_path", + [ + pytest.param("@google.com", "/@google.com", id="at-sign"), + pytest.param("user:pass@google.com", "/user:pass@google.com", id="userinfo"), + pytest.param("//google.com/x", "//google.com/x", id="scheme-relative"), + pytest.param("http://google.com/x", "/http://google.com/x", id="absolute"), + ], +) +@pytest.mark.parametrize("with_host_header", [True, False], ids=["host-header", "server-fallback"]) +def test_url_from_scope_with_authority_in_path(path: str, expected_path: str, with_host_header: bool) -> None: + """A path must not bleed into the authority.""" + headers = [(b"host", b"localhost")] if with_host_header else [] + u = URL( + scope={ + "scheme": "http", + "server": ("localhost", 80), + "path": path, + "query_string": b"a=b", + "headers": headers, + } + ) + assert u.hostname == "localhost" + assert u.path == expected_path + assert u.query == "a=b" + + def test_headers() -> None: h = Headers(raw=[(b"a", b"123"), (b"a", b"456"), (b"b", b"789")]) assert "a" in h
Vulnerability mechanics
Root cause
"Missing validation that the HTTP request path begins with `/` before concatenating it into the URL, allowing the path to be re-parsed as authority components."
Attack vector
An attacker sends an HTTP request whose request-target (the path component) does not start with `/`, for example `GET @google.com HTTP/1.1`. The affected code reconstructs `request.url` by concatenating `{scheme}://{host}{path}` without validating that the path begins with `/`. When the resulting string is re-parsed, the substring before `@` is interpreted as userinfo per RFC 3986, so the attacker-controlled portion becomes the hostname. Any downstream code that reads `request.url.hostname` or `request.url.netloc` for security decisions (host-based authorization, SSRF prevention, cache keys) can be misled. [CWE-20] [CWE-706]
What the fix does
The patch replaces string concatenation with `SplitResult(...).geturl()` from the `urllib.parse` module, which properly encodes the path component so it cannot bleed into the authority. Instead of building `f"{scheme}://{host_header}{path}"`, the code now constructs a `SplitResult` with separate `scheme`, `netloc`, `path`, and `query` fields, then calls `.geturl()` to produce the canonical URL. This ensures that a path like `@google.com` is rendered as `/@google.com` (with a leading slash) and the hostname remains `localhost`. The test suite confirms that paths containing `@`, `user:pass@`, `//`, or `http://` are all safely handled. [patch_id=6110789]
Preconditions
- inputThe ASGI server must forward a request-target lacking a leading `/` into `scope["path"]`
- networkNo fronting proxy or load balancer rejects the malformed request-target before it reaches the application
- configThe application must read `request.url`, `request.url.netloc`, or `request.url.hostname` for a security-sensitive decision
Generated on Jun 15, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
2News mentions
0No linked articles in our index yet.