diff --git a/gin.go b/gin.go index 2e033bf3..93c040ae 100644 --- a/gin.go +++ b/gin.go @@ -485,8 +485,8 @@ func (engine *Engine) validateHeader(header string) (clientIP string, valid bool } items := strings.Split(header, ",") for i := len(items) - 1; i >= 0; i-- { - ipStr := strings.TrimSpace(items[i]) - ip := net.ParseIP(ipStr) + item := strings.TrimSpace(items[i]) + ipStr, ip := parseForwardedForItem(item) if ip == nil { break } @@ -500,6 +500,35 @@ func (engine *Engine) validateHeader(header string) (clientIP string, valid bool return "", false } +// parseForwardedForItem normalizes a single X-Forwarded-For entry and parses it. +// It accepts the four common forms emitted by reverse proxies: +// +// - "1.2.3.4" +// - "2001:db8::1" +// - "[2001:db8::1]" (IIS/ARR style) +// - "1.2.3.4:12345" (with port, some LBs) +// - "[2001:db8::1]:12345" (IIS/ARR + port) +// +// The returned string is the IP without brackets or port, so callers see a +// consistent form regardless of which proxy produced the header. +func parseForwardedForItem(item string) (string, net.IP) { + // Try host:port form first (handles "ip:port" and "[ipv6]:port"). + if host, _, err := net.SplitHostPort(item); err == nil { + if ip := net.ParseIP(host); ip != nil { + return host, ip + } + } + // Strip optional surrounding brackets for bare "[ipv6]" with no port. + unbracketed := item + if strings.HasPrefix(unbracketed, "[") && strings.HasSuffix(unbracketed, "]") { + unbracketed = unbracketed[1 : len(unbracketed)-1] + } + if ip := net.ParseIP(unbracketed); ip != nil { + return unbracketed, ip + } + return "", nil +} + // updateRouteTree do update to the route tree recursively func updateRouteTree(n *node) { n.path = strings.ReplaceAll(n.path, escapedColon, colon) diff --git a/gin_test.go b/gin_test.go index a9cf1755..41f671e5 100644 --- a/gin_test.go +++ b/gin_test.go @@ -1156,3 +1156,35 @@ func TestUpdateRouteTreesCalledOnce(t *testing.T) { assert.Equal(t, "ok", w.Body.String()) } } + +func TestValidateHeaderForwardedForForms(t *testing.T) { + engine := New() + // Disable trusted proxies so the rightmost parseable entry is returned. + require.NoError(t, engine.SetTrustedProxies(nil)) + + tests := []struct { + name string + header string + wantIP string + wantOK bool + }{ + {"plain IPv4", "192.168.8.39", "192.168.8.39", true}, + {"plain IPv6", "240e:318:2f4a:de56::240", "240e:318:2f4a:de56::240", true}, + {"bracketed IPv6 (IIS/ARR)", "[240e:318:2f4a:de56::240]", "240e:318:2f4a:de56::240", true}, + {"IPv4 with port", "192.168.8.39:38792", "192.168.8.39", true}, + {"bracketed IPv6 with port", "[240e:318:2f4a:de56::240]:38792", "240e:318:2f4a:de56::240", true}, + {"IPv6 loopback bracketed", "[::1]", "::1", true}, + {"chain with port on last entry", "1.2.3.4, 5.6.7.8:9000", "5.6.7.8", true}, + {"empty", "", "", false}, + {"garbage", "not-an-ip", "", false}, + {"bracketed garbage", "[not-an-ip]", "", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotIP, gotOK := engine.validateHeader(tt.header) + assert.Equal(t, tt.wantOK, gotOK) + assert.Equal(t, tt.wantIP, gotIP) + }) + } +}