rproxy: remove every X-Forwarded- header from an untrusted peer (closes #325)
check / check (push) Waiting to run

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
This commit is contained in:
clawbot committed 2026-11-11 04:50:31 +00:00
1 parent f7e3190654
commit 35ccb256f9
3 files changed
+18 -12

No files matched your search

+3 -2
View File
@@ -2310,8 +2310,9 @@ but for these headers:
on. on.
- For a peer outside `SWRP_TRUSTED_PROXIES`, its forwarded headers are its own - 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` 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 the host it asked for, and `X-Forwarded-Proto` plain `http`. Every other
in which traefik tells an app about a visitor are removed. header whose name starts with `X-Forwarded-` is removed, and so is
`X-Real-IP`.
- `X-Request-ID`, which carries the fresh id. - `X-Request-ID`, which carries the fresh id.
- `X-Client-ASN` and `X-Client-Country`, which carry what `waf` answered. A - `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 header it did not give is left out. Any of the two the visitor sent is
+8 -9
View File
@@ -105,17 +105,16 @@ func setForwardedHeaders(in, out *http.Request, peer netip.Addr, trusted bool) {
return return
} }
// ReverseProxy has removed Forwarded and the three set below; these are the // ReverseProxy has removed Forwarded and the three set below. Go's server
// other headers in which traefik tells the app about the visitor and its // gives header names in canonical form, so the prefix matches any spelling.
// request. for name := range out.Header {
for _, name := range []string{ if strings.HasPrefix(name, "X-Forwarded-") {
"X-Forwarded-Port", "X-Forwarded-Server", "X-Forwarded-Uri", delete(out.Header, name)
"X-Forwarded-Method", "X-Forwarded-Prefix", "X-Forwarded-Tls-Client-Cert", }
"X-Forwarded-Tls-Client-Cert-Info", "X-Real-Ip",
} {
out.Header.Del(name)
} }
out.Header.Del("X-Real-IP")
out.Header.Set("X-Forwarded-For", forwardedFor) out.Header.Set("X-Forwarded-For", forwardedFor)
out.Header.Set("X-Forwarded-Host", in.Host) out.Header.Set("X-Forwarded-Host", in.Host)
out.Header.Set("X-Forwarded-Proto", "http") out.Header.Set("X-Forwarded-Proto", "http")
+7 -1
View File
@@ -559,6 +559,8 @@ func TestAPeerNotTrustedHasItsForwardedHeadersReplaced(t *testing.T) {
"X-Forwarded-Port", "X-Forwarded-Server", "X-Forwarded-Uri", "X-Forwarded-Port", "X-Forwarded-Server", "X-Forwarded-Uri",
"X-Forwarded-Method", "X-Forwarded-Prefix", "X-Forwarded-Tls-Client-Cert", "X-Forwarded-Method", "X-Forwarded-Prefix", "X-Forwarded-Tls-Client-Cert",
"X-Forwarded-Tls-Client-Cert-Info", "X-Real-Ip", "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) standIn := startWAF(t, allow)
@@ -582,6 +584,9 @@ func TestAPeerNotTrustedHasItsForwardedHeadersReplaced(t *testing.T) {
req.Header.Set(name, "claimed") 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)) wantGreeting(t, visit(t, req))
header := <-seen 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 { if got := header.Values(name); len(got) != 0 {
t.Errorf("the app saw %s %q, want none", name, got) 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", "X-Forwarded-Host": "public.example",
forwardedProtoHeader: "https", forwardedProtoHeader: "https",
"X-Real-Ip": "198.51.100.1", "X-Real-Ip": "198.51.100.1",
"X-Forwarded-Ssl": "on",
} }
req := newRequest(t, http.MethodGet, addr, "/", http.NoBody) req := newRequest(t, http.MethodGet, addr, "/", http.NoBody)