diff --git a/context_test.go b/context_test.go index ef60379d..38cf451f 100644 --- a/context_test.go +++ b/context_test.go @@ -2112,6 +2112,44 @@ func TestContextClientIP(t *testing.T) { assert.Empty(t, c.ClientIP()) } +func TestContextClientIPWithNonstandardXForwardedFor(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + c.Request, _ = http.NewRequest(http.MethodGet, "/", nil) + c.engine.trustedCIDRs = defaultTrustedCIDRs + c.engine.ForwardedByClientIP = true + c.engine.RemoteIPHeaders = []string{"X-Forwarded-For"} + c.Request.RemoteAddr = "127.0.0.1:1234" + + // IPv4 with port (e.g., IIS ARR with TCP port enabled) + c.Request.Header.Set("X-Forwarded-For", "192.168.8.39:38792") + assert.Equal(t, "192.168.8.39", c.ClientIP()) + + // IPv6 with brackets (e.g., IIS reverse proxy) + c.Request.Header.Set("X-Forwarded-For", "[240e:318:2f4a:de56::240]") + assert.Equal(t, "240e:318:2f4a:de56::240", c.ClientIP()) + + // IPv6 with brackets and port + c.Request.Header.Set("X-Forwarded-For", "[240e:318:2f4a:de56::240]:38792") + assert.Equal(t, "240e:318:2f4a:de56::240", c.ClientIP()) + + // Standard IPv4 still works + c.Request.Header.Set("X-Forwarded-For", "192.168.8.39") + assert.Equal(t, "192.168.8.39", c.ClientIP()) + + // Standard IPv6 still works + c.Request.Header.Set("X-Forwarded-For", "240e:318:2f4a:de56::240") + assert.Equal(t, "240e:318:2f4a:de56::240", c.ClientIP()) + + // Multiple IPs with non-standard format (mixed) + _ = c.engine.SetTrustedProxies([]string{"127.0.0.1", "30.30.30.30"}) + c.Request.Header.Set("X-Forwarded-For", "192.168.8.39:38792, 30.30.30.30") + assert.Equal(t, "192.168.8.39", c.ClientIP()) + + // Multiple IPs with bracketed IPv6 + c.Request.Header.Set("X-Forwarded-For", "[240e:318:2f4a:de56::240]:38792, 30.30.30.30") + assert.Equal(t, "240e:318:2f4a:de56::240", c.ClientIP()) +} + func resetContextForClientIPTests(c *Context) { c.Request.Header.Set("X-Real-IP", " 10.10.10.10 ") c.Request.Header.Set("X-Forwarded-For", " 20.20.20.20, 30.30.30.30") diff --git a/gin.go b/gin.go index 2e033bf3..9ca63a24 100644 --- a/gin.go +++ b/gin.go @@ -488,7 +488,14 @@ func (engine *Engine) validateHeader(header string) (clientIP string, valid bool ipStr := strings.TrimSpace(items[i]) ip := net.ParseIP(ipStr) if ip == nil { - break + // Try to parse non-standard formats: + // - IPv4 with port: "192.168.1.1:8080" + // - IPv6 with brackets: "[::1]" + // - IPv6 with brackets and port: "[::1]:8080" + ip, ipStr = parseNonstandardIP(ipStr) + if ip == nil { + break + } } // X-Forwarded-For is appended by proxy @@ -500,6 +507,36 @@ func (engine *Engine) validateHeader(header string) (clientIP string, valid bool return "", false } +// parseNonstandardIP parses non-standard IP formats commonly found in +// X-Forwarded-For headers set by reverse proxies like IIS or cloud load balancers. +// It handles: +// - IPv4 with port: "192.168.1.1:8080" +// - IPv6 with brackets: "[::1]" +// - IPv6 with brackets and port: "[::1]:8080" +// +// Returns the parsed net.IP and the clean IP string, or (nil, "") if parsing fails. +func parseNonstandardIP(raw string) (net.IP, string) { + // Try net.SplitHostPort to handle "ip:port" and "[ip]:port" formats + host, _, err := net.SplitHostPort(raw) + if err == nil { + ip := net.ParseIP(host) + if ip != nil { + return ip, host + } + } + + // Handle bare bracketed IPv6 without port: "[::1]" + if len(raw) > 2 && raw[0] == '[' && raw[len(raw)-1] == ']' { + host := raw[1 : len(raw)-1] + ip := net.ParseIP(host) + if ip != nil { + return ip, host + } + } + + 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..8c8e2822 100644 --- a/gin_test.go +++ b/gin_test.go @@ -1156,3 +1156,80 @@ func TestUpdateRouteTreesCalledOnce(t *testing.T) { assert.Equal(t, "ok", w.Body.String()) } } + +func TestParseNonstandardIP(t *testing.T) { + tests := []struct { + name string + input string + wantIP bool + wantAddr string + }{ + // IPv4 with port + {"ipv4 with port", "192.168.8.39:38792", true, "192.168.8.39"}, + // IPv6 with brackets + {"ipv6 with brackets", "[240e:318:2f4a:de56::240]", true, "240e:318:2f4a:de56::240"}, + // IPv6 with brackets and port + {"ipv6 with brackets and port", "[240e:318:2f4a:de56::240]:38792", true, "240e:318:2f4a:de56::240"}, + // Loopback IPv6 with brackets + {"loopback ipv6 with brackets", "[::1]", true, "::1"}, + // Loopback IPv6 with brackets and port + {"loopback ipv6 with brackets and port", "[::1]:8080", true, "::1"}, + // Plain IPv4 (should fail - not non-standard) + {"plain ipv4", "192.168.1.1", false, ""}, + // Plain IPv6 (should fail - not non-standard) + {"plain ipv6", "::1", false, ""}, + // Invalid input + {"invalid", "not-an-ip", false, ""}, + // Empty string + {"empty", "", false, ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ip, addr := parseNonstandardIP(tt.input) + if tt.wantIP { + assert.NotNil(t, ip, "expected non-nil IP for input %q", tt.input) + assert.Equal(t, tt.wantAddr, addr) + } else { + assert.Nil(t, ip, "expected nil IP for input %q", tt.input) + assert.Empty(t, addr) + } + }) + } +} + +func TestValidateHeaderWithNonstandardIPs(t *testing.T) { + engine := New() + _ = engine.SetTrustedProxies([]string{"0.0.0.0/0", "::/0"}) + + tests := []struct { + name string + header string + wantIP string + wantOK bool + }{ + // Standard formats (should still work) + {"standard ipv4", "192.168.1.1", "192.168.1.1", true}, + {"standard ipv6", "240e:318:2f4a:de56::240", "240e:318:2f4a:de56::240", true}, + // Non-standard formats + {"ipv4 with port", "192.168.8.39:38792", "192.168.8.39", true}, + {"ipv6 with brackets", "[240e:318:2f4a:de56::240]", "240e:318:2f4a:de56::240", true}, + {"ipv6 with brackets and port", "[240e:318:2f4a:de56::240]:38792", "240e:318:2f4a:de56::240", true}, + // Mixed with multiple IPs + {"mixed standard and nonstandard", "192.168.8.39:38792, 10.0.0.1", "192.168.8.39", true}, + {"mixed bracketed ipv6 and standard", "[::1]:8080, 10.0.0.1", "::1", true}, + // Invalid header + {"empty header", "", "", false}, + {"completely invalid", "not-an-ip", "", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ip, valid := engine.validateHeader(tt.header) + assert.Equal(t, tt.wantOK, valid, "valid mismatch for header %q", tt.header) + if tt.wantOK { + assert.Equal(t, tt.wantIP, ip, "IP mismatch for header %q", tt.header) + } + }) + } +}