From 35ccb256f945214642d22a53fd68c2dd5486fe24 Mon Sep 17 00:00:00 2001 From: clawbot Date: Wed, 11 Nov 2026 04:50:31 +0000 Subject: [PATCH] rproxy: remove every X-Forwarded- header from an untrusted peer (closes #325) For a peer outside SWRP_TRUSTED_PROXIES, rproxy removed a fixed list of traefik's forwarded headers, so names such as X-Forwarded-Ssl and X-Forwarded-Scheme reached the app as the client sent them. An app that reads them could take a plain request for a secure one. Now every header whose name starts with X-Forwarded- is removed before rproxy sets its three, and X-Real-IP as before. A trusted peer's headers pass on unchanged. Model: opus-5-5 --- SPEC.md | 5 +++-- internal/rproxy/client.go | 17 ++++++++--------- internal/rproxy/rproxy_test.go | 8 +++++++- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/SPEC.md b/SPEC.md index fb56d77..9ddaa15 100644 --- a/SPEC.md +++ b/SPEC.md @@ -2310,8 +2310,9 @@ but for these headers: on. - For a peer outside `SWRP_TRUSTED_PROXIES`, its forwarded headers are its own claims and are replaced. `X-Forwarded-For` names the peer, `X-Forwarded-Host` - the host it asked for, and `X-Forwarded-Proto` plain `http`. The other headers - in which traefik tells an app about a visitor are removed. + the host it asked for, and `X-Forwarded-Proto` plain `http`. Every other + header whose name starts with `X-Forwarded-` is removed, and so is + `X-Real-IP`. - `X-Request-ID`, which carries the fresh id. - `X-Client-ASN` and `X-Client-Country`, which carry what `waf` answered. A header it did not give is left out. Any of the two the visitor sent is diff --git a/internal/rproxy/client.go b/internal/rproxy/client.go index e569bbf..a10fbc7 100644 --- a/internal/rproxy/client.go +++ b/internal/rproxy/client.go @@ -105,17 +105,16 @@ func setForwardedHeaders(in, out *http.Request, peer netip.Addr, trusted bool) { return } - // ReverseProxy has removed Forwarded and the three set below; these are the - // other headers in which traefik tells the app about the visitor and its - // request. - for _, name := range []string{ - "X-Forwarded-Port", "X-Forwarded-Server", "X-Forwarded-Uri", - "X-Forwarded-Method", "X-Forwarded-Prefix", "X-Forwarded-Tls-Client-Cert", - "X-Forwarded-Tls-Client-Cert-Info", "X-Real-Ip", - } { - out.Header.Del(name) + // ReverseProxy has removed Forwarded and the three set below. Go's server + // gives header names in canonical form, so the prefix matches any spelling. + for name := range out.Header { + if strings.HasPrefix(name, "X-Forwarded-") { + delete(out.Header, name) + } } + out.Header.Del("X-Real-IP") + out.Header.Set("X-Forwarded-For", forwardedFor) out.Header.Set("X-Forwarded-Host", in.Host) out.Header.Set("X-Forwarded-Proto", "http") diff --git a/internal/rproxy/rproxy_test.go b/internal/rproxy/rproxy_test.go index 0e6e2e8..4b1fec0 100644 --- a/internal/rproxy/rproxy_test.go +++ b/internal/rproxy/rproxy_test.go @@ -559,6 +559,8 @@ func TestAPeerNotTrustedHasItsForwardedHeadersReplaced(t *testing.T) { "X-Forwarded-Port", "X-Forwarded-Server", "X-Forwarded-Uri", "X-Forwarded-Method", "X-Forwarded-Prefix", "X-Forwarded-Tls-Client-Cert", "X-Forwarded-Tls-Client-Cert-Info", "X-Real-Ip", + "X-Forwarded-Scheme", "X-Forwarded-Ssl", "X-Forwarded-Protocol", + "X-Forwarded-Path", "X-Forwarded-Client-Cert", } standIn := startWAF(t, allow) @@ -582,6 +584,9 @@ func TestAPeerNotTrustedHasItsForwardedHeadersReplaced(t *testing.T) { req.Header.Set(name, "claimed") } + // A made-up name, sent in lower case. + req.Header["x-forwarded-made-up"] = []string{"claimed"} + wantGreeting(t, visit(t, req)) header := <-seen @@ -596,7 +601,7 @@ func TestAPeerNotTrustedHasItsForwardedHeadersReplaced(t *testing.T) { } } - for _, name := range removed { + for _, name := range append(removed, "X-Forwarded-Made-Up") { if got := header.Values(name); len(got) != 0 { t.Errorf("the app saw %s %q, want none", name, got) } @@ -622,6 +627,7 @@ func TestATrustedProxysForwardedHeadersPassOn(t *testing.T) { "X-Forwarded-Host": "public.example", forwardedProtoHeader: "https", "X-Real-Ip": "198.51.100.1", + "X-Forwarded-Ssl": "on", } req := newRequest(t, http.MethodGet, addr, "/", http.NoBody)