Fix X-Forwarded-For not properly handling ports#2823
Fix X-Forwarded-For not properly handling ports#2823worksbyfriday wants to merge 1 commit intoKludex:mainfrom
Conversation
When X-Forwarded-For entries include a port (e.g. "1.2.3.4:1024"),
the port was being included in the host field of scope["client"],
producing malformed tuples like ("1.2.3.4:1024", 0). Additionally,
the trust check was failing because ipaddress.ip_address() cannot
parse "1.2.3.4:1024".
Add _parse_host_and_port() to extract host and port from forwarded
entries, handling IPv4 (host:port), bracketed IPv6 ([::1]:port),
and bare addresses without ports.
Fixes Kludex#2789
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
hrv-dys
left a comment
There was a problem hiding this comment.
This tackles a real gap — X-Forwarded-For values can include ports (e.g., from AWS ALB or HAProxy), and the current code just passes the whole string as the host, which breaks the client tuple semantics.
On the implementation:
-
_parse_host_and_port()— the function handles three cases: bracketed IPv6 ([::1]:8080), IPv4 with exactly one colon (1.2.3.4:8080), and bare hosts. Thehost.count(":") == 1check for IPv4 is a good heuristic to distinguish1.2.3.4:8080from bare IPv6 like::1. Nice. -
Trust checking — the change at line 166 (
addr, _ = _parse_host_and_port(host)) strips the port before checking against trusted hosts. This is critical — without it,"10.0.0.1:8080"would fail the__contains__check against"10.0.0.1"in the trusted set, and trusted proxies with ports would be treated as untrusted. The testtest_proxy_headers_x_forwarded_for_port_with_trusted_proxyvalidates exactly this. -
The return value preserves the full
host:portstring as the client host — wait, actually looking again, you're returning(addr, port)from_parse_host_and_portbut inget_trusted_client_hostyou still return the rawhoststring (which includes the port). So thescope["client"]tuple becomes("1.2.3.4:8080", port_int)— hmm, actually no, looking at the middleware code:addr, port = _parse_host_and_port(host)and thenscope["client"] = (addr, port). That's correct — the addr is just the IP, port is the integer. Good. -
Comparison with PR #2822 — that PR takes a regex-based approach with
_IPV6_HOST_PORTand_IPV4_HOST_PORTcompiled patterns and renamesget_trusted_client_hosttoget_trusted_client_host_portto return the tuple directly. Both approaches pass all 245 tests. This PR's approach is arguably more readable — the sequential if/elif in_parse_host_and_portis easier to follow than regex matching. On the other hand, #2822's approach avoids parsing twice (once for trust checking, once for the client tuple).
All 245 proxy header tests pass (8 skipped — wsproto not installed, pre-existing).
Summary
X-Forwarded-Forentries include a port (e.g.1.2.3.4:1024), the port was included in the host field ofscope["client"], producing malformed tuples like("1.2.3.4:1024", 0)ipaddress.ip_address()cannot parse1.2.3.4:1024_parse_host_and_port()helper to extract host and port separately, handling IPv4 (host:port), bracketed IPv6 ([::1]:port), and bare addressesFixes #2789
Test plan
test_proxy_headers_x_forwarded_for_with_port— IPv4 with port, IPv4 without port, bracketed IPv6 with port, bare IPv6test_proxy_headers_x_forwarded_for_port_with_trusted_proxy— verifies trust check works when proxy sendshost:port