Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
20 changes: 20 additions & 0 deletions gin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
35 changes: 35 additions & 0 deletions gin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
}
Loading