Skip to content

Bug: AllowedHostsMiddleware rejects valid hosts when the Host header includes a port #4869

Description

@fmoga

Description

AllowedHostsMiddleware used to strip the port from the Host header before matching it against the allowed-hosts regex. As of 2.22.0 it matches the full, un-stripped header, so a request with Host: localhost:8000 no longer matches an allowed host of localhost and is rejected with 400 - invalid host header.

The host check works correctly in production behind a proxy (where Host is the bare domain with no port), which is likely why this has gone unnoticed — it only manifests when the Host header carries a port.

Root cause

The port-strip was lost in c4189610 ("fix: fix typing after merge"), a follow-up to the intentional x-forwarded-host removal. The walrus now binds host to the un-stripped header value; host.split(":")[0] survives only as the truthiness guard on the left of the and, while fullmatch(host) receives the value with the port still attached:

# before (2.21.x) — port stripped, then matched
if host := headers.get("host").split(":")[0]:
    if self.allowed_hosts_regex.fullmatch(host):

# after (2.22.0+) — `host` keeps the port; the strip only gates the `if`
if (host := headers.get("host")) is not None and host.split(":")[0]:
    if self.allowed_hosts_regex.fullmatch(host):   # matches "localhost:8000"

Suggested fix

Match against the port-stripped value while keeping the None guard, e.g.:

if (raw_host := headers.get("host")) is not None and (host := raw_host.split(":")[0]):
    if self.allowed_hosts_regex.fullmatch(host):

URL to code causing the issue

No response

MCVE

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


@get("/")
async def handler() -> str:
    return "ok"


app = Litestar(
    route_handlers=[handler],
    allowed_hosts=AllowedHostsConfig(allowed_hosts=["localhost"]),
)


def test_host_with_port_is_allowed() -> None:
    # base_url drives the Host header -> "localhost:8000"
    with TestClient(app=app, base_url="http://localhost:8000") as client:
        response = client.get("/")
    assert response.status_code == 200  # passes on 2.21.x, fails (400) on >= 2.22.0

Steps to reproduce

Expected behavior

Host: localhost:8000 matches the allowed host localhost (port stripped before matching), and the request returns 200. This is the 2.21.x behavior, and matches the convention used by Django's ALLOWED_HOSTS and Starlette's TrustedHostMiddleware.

Actual behavior

The request is rejected with 400 - {"message":"invalid host header"} because the port is included in the matched value.

Screenshots

No response

Logs


Litestar Version

2.22.0 – 2.24.0 (still present on main)

Platform

  • Linux
  • Mac
  • Windows
  • Other (Please specify in the description above)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Bug 🐛This is something that is not working as expected

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions