Defect
src/health/server.py:82-95 trusts the left-most X-Forwarded-For segment verbatim whenever the immediate peer matches web.trusted_proxies. It neither validates the segment as an IP address nor walks the forwarding chain from the trusted side. The returned string is then used as the rate-limit bucket key at src/health/server.py:415-423 and persisted as the actor IP at src/health/server.py:530-539.
Concrete failure sequence
- Configure the dashboard behind a trusted reverse proxy that preserves/appends an incoming
X-Forwarded-For header.
- A caller sends more than 120
/api/ requests in one minute, changing the first header segment on every request (arbitrary strings are accepted).
- The proxy appends the real client address and forwards each request from its configured trusted peer.
_client_ip() returns the caller-controlled first segment each time.
_make_rate_limit_middleware() creates a fresh bucket for every request, so the documented 120-request limit is never reached. _make_web_audit_middleware() also records the spoofed value instead of the client address.
Malformed, empty, or ambiguous forwarding headers similarly should not become identity keys. Parse and validate forwarded addresses, derive the client by walking the chain from the trusted proxy side (with CIDR-aware trust if CIDRs are supported), and fall back to the immediate peer for malformed or ambiguous input. Add regression coverage that rotates an attacker-supplied left-most value through a trusted proxy and still reaches one stable rate-limit bucket.
Defect
src/health/server.py:82-95trusts the left-mostX-Forwarded-Forsegment verbatim whenever the immediate peer matchesweb.trusted_proxies. It neither validates the segment as an IP address nor walks the forwarding chain from the trusted side. The returned string is then used as the rate-limit bucket key atsrc/health/server.py:415-423and persisted as the actor IP atsrc/health/server.py:530-539.Concrete failure sequence
X-Forwarded-Forheader./api/requests in one minute, changing the first header segment on every request (arbitrary strings are accepted)._client_ip()returns the caller-controlled first segment each time._make_rate_limit_middleware()creates a fresh bucket for every request, so the documented 120-request limit is never reached._make_web_audit_middleware()also records the spoofed value instead of the client address.Malformed, empty, or ambiguous forwarding headers similarly should not become identity keys. Parse and validate forwarded addresses, derive the client by walking the chain from the trusted proxy side (with CIDR-aware trust if CIDRs are supported), and fall back to the immediate peer for malformed or ambiguous input. Add regression coverage that rotates an attacker-supplied left-most value through a trusted proxy and still reaches one stable rate-limit bucket.