Forwarded-trust hardening: X-Real-IP fallback, and no warning for a trusting listener with no match_local_ip #64
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
From the v2.0.0 security audit. Two related gaps in
trust_forwarded_for, both hardening rather than defects — the trust gate itself now holds on both the believe and send sides afterf02af79.1. The
X-Real-IPfallback assumes the intermediary overwrites itaccess::effective_client_iphonoursCF-Connecting-IPand falls back toX-Real-IP. That is only sound if the trusted intermediary overwrites or stripsX-Real-IP. Cloudflare and cloudflared setCF-Connecting-IP, which wins, so the documented deployment is fine.But README presents nginx, HAProxy and NLB front-ends as supported shapes, and nginx does not set
X-Real-IPunless configured to. An operator with HAProxy in front, atrust_forwarded_forlistener andallow_ips = ["100.64.0.0/10"]is then bypassed by a client sendingX-Real-IP: 100.64.0.1.The code path is certain; the exposure depends on the front-end, and no fleet deployment is known to hit it.
Fix: either gate the fallback behind an explicit per-listener choice of which header to trust, or document that a trusted intermediary MUST overwrite both names. The explicit choice is better — "which header carries the client IP" is a property of the intermediary, and guessing a priority order is what created this.
2. Nothing warns about the dangerous listener shape
There is no code notion of "directly reachable" — the invariant is structural and operator-asserted via the parent binding's address plus
match_local_ip. Zero-config is safe: all three synthesised-listener branches hard-settrust_forwarded_for: false.But
trust_forwarded_for = truewith nomatch_local_ip, on a binding bound to0.0.0.0, starts silently and lets every direct client spoof the client IP. That is the one configuration that breaks the invariant, and it is accepted without comment.Fix: a startup warning naming the listener. Cheap, and it is the only signal an operator would get.
3. Bare
split(':')on the admin Host headersrc/middleware.rs:102and:141parse the Host withh.split(':').next()rather thanproxy::authority_host, which CLAUDE.md names as a landmine. An IPv6-literal admin host ([fd00::1]:8443) yields"[", soadmin_host_gatenever matches and the admin API becomes unreachable on that host.No bypass —
admin_ip_middlewareuses the identical parse, andproxy_handlerusesauthority_hostcorrectly and only serves dashboard HTML after its own access check. Availability bug plus a deviation from a documented rule.