VYPR
Low severity3.7GHSA Advisory· Published Jun 15, 2026· Updated Jun 15, 2026

Starlette: Unvalidated request path concatenated into authority poisons request.url.hostname

CVE-2026-54282

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

2

Patches

1
167b5850e809

Build `request.url` from structured components (#3326)

https://github.com/kludex/starletteMarcelo TrylesinskiJun 11, 2026Fixed in 1.3.0via llm-release-walk
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

2

News mentions

0

No linked articles in our index yet.