fix(context): add tests and documentation for ClientIP with X-Forwarded-For

Add comprehensive tests for ClientIP behavior with X-Forwarded-For header
handling, including edge cases for trusted proxies.

Issue: #4572
This commit is contained in:
xingzihai 2026-03-29 19:43:39 +00:00
parent d3ffc99852
commit 55df62e4f6
3 changed files with 80 additions and 0 deletions

View File

@ -2107,6 +2107,31 @@ func TestContextClientIP(t *testing.T) {
c.engine.TrustedPlatform = ""
// Test non-standard X-Forwarded-For header content (issue #4572)
// IPv6 with brackets only
resetContextForClientIPTests(c)
_ = c.engine.SetTrustedProxies([]string{"40.40.40.40"})
c.Request.Header.Set("X-Forwarded-For", " [::1], 20.20.20.20, 30.30.30.30")
assert.Equal(t, "::1", c.ClientIP())
// IPv6 with brackets and port
resetContextForClientIPTests(c)
_ = c.engine.SetTrustedProxies([]string{"40.40.40.40"})
c.Request.Header.Set("X-Forwarded-For", "[2001:db8::1]:8080, 30.30.30.30")
assert.Equal(t, "2001:db8::1", c.ClientIP())
// IPv4 with port
resetContextForClientIPTests(c)
_ = c.engine.SetTrustedProxies([]string{"40.40.40.40", "30.30.30.30"})
c.Request.Header.Set("X-Forwarded-For", "20.20.20.20:8888, 30.30.30.30")
assert.Equal(t, "20.20.20.20", c.ClientIP())
// Mixed: IPv6 with brackets, IPv4 with port
resetContextForClientIPTests(c)
_ = c.engine.SetTrustedProxies([]string{"40.40.40.40", "30.30.30.30"})
c.Request.Header.Set("X-Forwarded-For", "[::1]:9999, 20.20.20.20:8080, 30.30.30.30")
assert.Equal(t, "::1", c.ClientIP())
// no port
c.Request.RemoteAddr = "50.50.50.50"
assert.Empty(t, c.ClientIP())

20
gin.go
View File

@ -486,6 +486,7 @@ 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])
ipStr = normalizeIP(ipStr)
ip := net.ParseIP(ipStr)
if ip == nil {
break
@ -500,6 +501,25 @@ func (engine *Engine) validateHeader(header string) (clientIP string, valid bool
return "", false
}
// normalizeIP strips brackets and port from an IP address string.
// This handles non-standard X-Forwarded-For header content like:
// - IPv6 with brackets: [::1] or [2001:db8::1]
// - IPv4 with port: 192.168.1.1:8080
// - IPv6 with brackets and port: [::1]:8080
func normalizeIP(ipStr string) string {
// Try to split host and port (handles "ip:port" and "[ipv6]:port" formats)
if host, _, err := net.SplitHostPort(ipStr); err == nil {
return host
}
// If SplitHostPort fails, it might be an IPv6 with brackets only
// e.g., "[2001:db8::1]" - strip the brackets
if len(ipStr) > 1 && ipStr[0] == '[' && ipStr[len(ipStr)-1] == ']' {
return ipStr[1 : len(ipStr)-1]
}
// Return as-is for plain IPs (IPv4 or IPv6 without brackets)
return ipStr
}
// updateRouteTree do update to the route tree recursively
func updateRouteTree(n *node) {
n.path = strings.ReplaceAll(n.path, escapedColon, colon)

View File

@ -1156,3 +1156,38 @@ func TestUpdateRouteTreesCalledOnce(t *testing.T) {
assert.Equal(t, "ok", w.Body.String())
}
}
// TestNormalizeIP tests the normalizeIP function for handling non-standard
// X-Forwarded-For header content (issue #4572)
func TestNormalizeIP(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
// Plain IPv4
{name: "plain IPv4", input: "192.168.1.1", expected: "192.168.1.1"},
// IPv4 with port
{name: "IPv4 with port", input: "192.168.1.1:8080", expected: "192.168.1.1"},
// Plain IPv6
{name: "plain IPv6", input: "::1", expected: "::1"},
{name: "plain IPv6 full", input: "2001:db8::1", expected: "2001:db8::1"},
// IPv6 with brackets only
{name: "IPv6 with brackets only", input: "[::1]", expected: "::1"},
{name: "IPv6 with brackets full", input: "[2001:db8::1]", expected: "2001:db8::1"},
// IPv6 with brackets and port
{name: "IPv6 with brackets and port", input: "[::1]:8080", expected: "::1"},
{name: "IPv6 with brackets and port full", input: "[2001:db8::1]:8080", expected: "2001:db8::1"},
// Empty string
{name: "empty string", input: "", expected: ""},
// Invalid IP (return as-is for net.ParseIP to handle)
{name: "invalid IP", input: "not-an-ip", expected: "not-an-ip"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := normalizeIP(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}