The Exploit
The attacker only needs the ability to send HTTP requests to the Starlette application.
curl -i -s -X GET 'http://127.0.0.1:8000/admin' \
-H 'Host: example.com/foo'
The app accepts the malformed Host header and still routes the raw request path /admin, returning the protected endpoint content. This proves that request.url was rebuilt from an attacker-controlled Host header and could disagree with the actual scope["path"] used for routing.
What the Patch Did
Before:
from collections.abc import ItemsView, Iterable, Iterator, KeysView, Mapping, MutableMapping, Sequence, ValuesView
from shlex import shlex
from typing import Any, BinaryIO, NamedTuple, TypeVar, cast
from urllib.parse import SplitResult, parse_qsl, urlencode, urlsplit
## Rejects Host header chars (/, ?, #, @, ...) that would let urlsplit produce a path differing from scope["path"].
class URL:
...
if host_header is not None:
url = f"{scheme}://{host_header}{path}"
After:
import re
from collections.abc import ItemsView, Iterable, Iterator, KeysView, Mapping, MutableMapping, Sequence, ValuesView
from shlex import shlex
from typing import Any, BinaryIO, NamedTuple, TypeVar, cast
from urllib.parse import SplitResult, parse_qsl, urlencode, urlsplit
## Rejects Host header chars (/, ?, #, @, ...) that would let urlsplit produce a path differing from scope["path"].
_HOST_RE = re.compile(r"^([a-z0-9.-]+|\[[a-f0-9]*:[a-f0-9.:]+\])(?::[0-9]+)?$", re.IGNORECASE)
class URL:
...
if host_header is not None and _HOST_RE.fullmatch(host_header):
url = f"{scheme}://{host_header}{path}"
The patch adds explicit validation of the Host header with _HOST_RE.fullmatch(host_header) before rebuilding request.url. Invalid Host values are ignored, forcing the URL builder to fall back to the trusted scope["server"] tuple.
Root Cause
This is an input-validation / request-parsing bug (CWE-20 / CWE-444) where attacker-controlled Host header data crosses a trust boundary unchecked. Starlette reconstructed request.url from raw scope["headers"] host_header values and scope["path"], while routing continued to use the original raw HTTP path. A malformed Host header like example.com/foo could change the URL parser’s interpretation of the path without changing the actual route selected, allowing security checks based on request.url to be bypassed.
Why It Works
The load-bearing fix is the and _HOST_RE.fullmatch(host_header) condition. If that line is removed, host_header is still accepted and url = f"{scheme}://{host_header}{path} will reconstruct a URL containing invalid path characters from the header. The regex constant _HOST_RE is the defence; the import and comment simply make the validation explicit and document why malformed hosts are rejected. Without the regex check, the library would still trust attacker-supplied Host values and the mismatch between request.url.path and scope["path"] remains exploitable.
Hardening Checklist
- Validate
Hostheader values against RFC 9112 / RFC 3986 host syntax before using them to rebuild URLs. - Use
scope["server"]or another trusted canonical host when header validation fails, instead of using malformed header data. - Keep routing decisions and authorization checks based on the same canonical request representation, not mixed derived values.
- Treat all HTTP request headers as attacker-controlled input; do not rely on
Hostfor access control or security policy decisions. - Add regression tests for invalid
Hostheaders such asfoo/?x=,foo/#,foo/bar,user@foo,foo\bar, andfoo bar.
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-48710