From 9920093c228f7b99b741c8eea0a124ca1c024153 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Wed, 29 Apr 2026 14:16:37 +0400 Subject: [PATCH] Use rightmost IP from forwarded headers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A client can prepend a spoofed entry to X-Forwarded-For before the request reaches our load balancer. Taking the first value would return the attacker's address. Since we sit behind a single trusted LB that appends the real client IP as the last entry, switch to rightmost extraction for both X-Forwarded-For and RFC 7239 Forwarded headers. Signed-off-by: Émile Ré --- pkg/server/api/clientip/clientip.go | 14 ++++++++------ pkg/server/api/clientip/clientip_test.go | 12 +++++++++--- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/pkg/server/api/clientip/clientip.go b/pkg/server/api/clientip/clientip.go index dd465ebe2..287165aed 100644 --- a/pkg/server/api/clientip/clientip.go +++ b/pkg/server/api/clientip/clientip.go @@ -22,7 +22,9 @@ import ( // Extract resolves the client IP address from standard proxy headers // in priority order: RFC 7239 Forwarded, then X-Forwarded-For, then -// the connection's remote address. +// the connection's remote address. It takes the rightmost (last) +// entry from multi-value headers — the one appended by the trusted +// load balancer closest to us. func Extract(r *http.Request) string { if fwd := r.Header.Get("Forwarded"); fwd != "" { if ip := parseForwardedFor(fwd); ip != "" { @@ -31,8 +33,8 @@ func Extract(r *http.Request) string { } if xff := r.Header.Get("X-Forwarded-For"); xff != "" { - if i := strings.IndexByte(xff, ','); i != -1 { - xff = xff[:i] + if i := strings.LastIndexByte(xff, ','); i != -1 { + xff = xff[i+1:] } xff = strings.TrimSpace(xff) @@ -50,11 +52,11 @@ func Extract(r *http.Request) string { return ip } -// parseForwardedFor extracts the client IP from the first "for=" directive +// parseForwardedFor extracts the client IP from the last "for=" directive // of an RFC 7239 Forwarded header value. func parseForwardedFor(header string) string { - if i := strings.IndexByte(header, ','); i != -1 { - header = header[:i] + if i := strings.LastIndexByte(header, ','); i != -1 { + header = header[i+1:] } for part := range strings.SplitSeq(header, ";") { diff --git a/pkg/server/api/clientip/clientip_test.go b/pkg/server/api/clientip/clientip_test.go index bcd116b1c..dd5b994b9 100644 --- a/pkg/server/api/clientip/clientip_test.go +++ b/pkg/server/api/clientip/clientip_test.go @@ -48,9 +48,15 @@ func TestExtract(t *testing.T) { want: "203.0.113.50", }, { - name: "x-forwarded-for chain", + name: "x-forwarded-for chain takes rightmost", remoteAddr: "10.0.0.1:1234", headers: map[string]string{"X-Forwarded-For": "203.0.113.50, 70.41.3.18, 150.172.238.178"}, + want: "150.172.238.178", + }, + { + name: "x-forwarded-for spoofed prefix", + remoteAddr: "10.0.0.1:1234", + headers: map[string]string{"X-Forwarded-For": "1.2.3.4, 203.0.113.50"}, want: "203.0.113.50", }, { @@ -84,10 +90,10 @@ func TestExtract(t *testing.T) { want: "198.51.100.17", }, { - name: "forwarded header chain", + name: "forwarded header chain takes rightmost", remoteAddr: "10.0.0.1:1234", headers: map[string]string{"Forwarded": "for=198.51.100.17, for=70.41.3.18"}, - want: "198.51.100.17", + want: "70.41.3.18", }, { name: "forwarded takes precedence over x-forwarded-for",