rproxy: remove forwarded headers spelt with underscores from an untrusted peer (closes #334)
check / check (push) Waiting to run

For a peer outside `SWRP_TRUSTED_PROXIES`, `rproxy` now reads each header name with `_` as `-` and in any letter case, and removes it when it starts with `X-Forwarded-` or is `Forwarded` or `X-Real-IP`. An app behind CGI or FastCGI reads `X_Forwarded_Ssl` as `HTTP_X_FORWARDED_SSL`, the same as `X-Forwarded-Ssl`, so a client connecting directly could still make it take a plain request for a secure one. A trusted peer's headers pass on unchanged. `SPEC.md` says so.

Model: opus-5-5
This commit is contained in:
sneak committed 2026-11-11 06:58:51 +00:00
1 parent eba697af48
commit e65a0f6087
3 files changed
+9 -7

No files matched your search

+2 -1
View File
@@ -2326,7 +2326,8 @@ but for these headers:
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`. Every other the host it asked for, and `X-Forwarded-Proto` plain `http`. Every other
header whose name starts with `X-Forwarded-` is removed, and so are header whose name starts with `X-Forwarded-` is removed, and so are
`Forwarded` and `X-Real-IP`. `Forwarded` and `X-Real-IP`. A name spelt with `_` for `-`, such as
`X_Forwarded_Ssl`, counts as the same name.
- `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
+5 -6
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. Go's server // An app behind CGI or FastCGI reads X_Forwarded_Ssl as X-Forwarded-Ssl, so
// gives header names in canonical form, so the prefix matches the name in // a name is matched with each _ read as - and in any letter case.
// any letter case.
for name := range out.Header { for name := range out.Header {
if strings.HasPrefix(name, "X-Forwarded-") { spelt := strings.ToLower(strings.ReplaceAll(name, "_", "-"))
if strings.HasPrefix(spelt, "x-forwarded-") ||
spelt == "forwarded" || spelt == "x-real-ip" {
delete(out.Header, name) delete(out.Header, 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")
+2
View File
@@ -561,6 +561,7 @@ func TestAPeerNotTrustedHasItsForwardedHeadersReplaced(t *testing.T) {
"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-Scheme", "X-Forwarded-Ssl", "X-Forwarded-Protocol",
"X-Forwarded-Path", "X-Forwarded-Client-Cert", "X-Forwarded-Path", "X-Forwarded-Client-Cert",
"X_Forwarded_Ssl", "X_Forwarded_Proto", "X_Real_IP",
} }
standIn := startWAF(t, allow) standIn := startWAF(t, allow)
@@ -628,6 +629,7 @@ func TestATrustedProxysForwardedHeadersPassOn(t *testing.T) {
forwardedProtoHeader: "https", forwardedProtoHeader: "https",
"X-Real-Ip": "198.51.100.1", "X-Real-Ip": "198.51.100.1",
"X-Forwarded-Ssl": "on", "X-Forwarded-Ssl": "on",
"X_Forwarded_Ssl": "on",
} }
req := newRequest(t, http.MethodGet, addr, "/", http.NoBody) req := newRequest(t, http.MethodGet, addr, "/", http.NoBody)