fix: support non-standard X-Forwarded-For header formats (#4572)

This fix addresses issue #4572 where ClientIP method fails to parse
X-Forwarded-For headers with non-standard formats:
- IPv6 addresses with square brackets: [240e:318:2f4a:de56::240]
- IPv4 addresses with port: 192.168.8.39:38792
- IPv6 addresses with square brackets and port: [240e:318:2f4a:de56::240]:38792

Changes:
- Modified validateHeader() to normalize IP strings before parsing
- Added normalizeIPFromHeader() helper function to handle various formats
- Added tryRemoveBracketsAndParse() helper function for bracket removal
- Added comprehensive test cases for all non-standard formats

The fix uses net.SplitHostPort to handle IP:port and [IPv6]:port formats,
and manual bracket removal for [IPv6] format (which SplitHostPort doesn't handle).
This commit is contained in:
xingzihai 2026-03-30 03:33:43 +00:00
parent d3ffc99852
commit 4ba0ba767c
2 changed files with 297 additions and 0 deletions

42
gin.go
View File

@ -487,6 +487,13 @@ func (engine *Engine) validateHeader(header string) (clientIP string, valid bool
for i := len(items) - 1; i >= 0; i-- {
ipStr := strings.TrimSpace(items[i])
ip := net.ParseIP(ipStr)
if ip == nil {
// Try to normalize the IP string. Some reverse proxies may send:
// 1. IPv6 addresses with square brackets: [240e:318:2f4a:de56::240]
// 2. IPv4 addresses with port: 192.168.8.39:38792
// 3. IPv6 addresses with square brackets and port: [240e:318:2f4a:de56::240]:38792
ipStr, ip = normalizeIPFromHeader(ipStr)
}
if ip == nil {
break
}
@ -500,6 +507,41 @@ func (engine *Engine) validateHeader(header string) (clientIP string, valid bool
return "", false
}
// normalizeIPFromHeader normalizes an IP string from X-Forwarded-For or similar headers.
// It handles various formats that reverse proxies may send:
// 1. IPv6 addresses with square brackets: [240e:318:2f4a:de56::240] -> 240e:318:2f4a:de56::240
// 2. IPv4 addresses with port: 192.168.8.39:38792 -> 192.168.8.39
// 3. IPv6 addresses with square brackets and port: [240e:318:2f4a:de56::240]:38792 -> 240e:318:2f4a:de56::240
// It returns the normalized IP string and the parsed net.IP.
func normalizeIPFromHeader(ipStr string) (string, net.IP) {
// First, try to split host and port (handles "IP:port" and "[IPv6]:port" formats)
host, _, err := net.SplitHostPort(ipStr)
if err == nil {
// Successfully split, now try to parse the host part
if ip := net.ParseIP(host); ip != nil {
return host, ip
}
// If host still has brackets (shouldn't happen, but be safe), try to remove them
return tryRemoveBracketsAndParse(host)
}
// SplitHostPort failed, the input might be just an IP with brackets: [IPv6]
return tryRemoveBracketsAndParse(ipStr)
}
// tryRemoveBracketsAndParse attempts to remove square brackets from an IP string and parse it.
// This handles cases like "[240e:318:2f4a:de56::240]" which some reverse proxies send.
func tryRemoveBracketsAndParse(ipStr string) (string, net.IP) {
// Check if the string is enclosed in square brackets
if len(ipStr) >= 2 && ipStr[0] == '[' && ipStr[len(ipStr)-1] == ']' {
inner := ipStr[1 : len(ipStr)-1]
if ip := net.ParseIP(inner); ip != nil {
return inner, ip
}
}
return "", nil
}
// 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,258 @@ func TestUpdateRouteTreesCalledOnce(t *testing.T) {
assert.Equal(t, "ok", w.Body.String())
}
}
// TestValidateHeaderWithNonStandardFormats tests that validateHeader can handle
// non-standard X-Forwarded-For header formats as described in issue #4572.
// These include:
// 1. IPv6 addresses with square brackets: [240e:318:2f4a:de56::240]
// 2. IPv4 addresses with port: 192.168.8.39:38792
// 3. IPv6 addresses with square brackets and port: [240e:318:2f4a:de56::240]:38792
func TestValidateHeaderWithNonStandardFormats(t *testing.T) {
SetMode(TestMode)
engine := New()
_ = engine.SetTrustedProxies([]string{"127.0.0.1", "::1"})
testCases := []struct {
name string
header string
expectedIP string
expectedValid bool
}{
// Standard formats (should already work)
{
name: "IPv4 plain",
header: "192.168.8.39",
expectedIP: "192.168.8.39",
expectedValid: true,
},
{
name: "IPv6 plain",
header: "240e:318:2f4a:de56::240",
expectedIP: "240e:318:2f4a:de56::240",
expectedValid: true,
},
// Non-standard formats (issue #4572)
{
name: "IPv6 with square brackets",
header: "[240e:318:2f4a:de56::240]",
expectedIP: "240e:318:2f4a:de56::240",
expectedValid: true,
},
{
name: "IPv4 with port",
header: "192.168.8.39:38792",
expectedIP: "192.168.8.39",
expectedValid: true,
},
{
name: "IPv6 with square brackets and port",
header: "[240e:318:2f4a:de56::240]:38792",
expectedIP: "240e:318:2f4a:de56::240",
expectedValid: true,
},
// Multiple entries in X-Forwarded-For with non-standard formats
{
name: "Multiple IPv6 with brackets",
header: "[240e:318:2f4a:de56::240], 127.0.0.1",
expectedIP: "240e:318:2f4a:de56::240",
expectedValid: true,
},
{
name: "Multiple IPv4 with ports",
header: "192.168.8.39:38792, 127.0.0.1:1234",
expectedIP: "192.168.8.39",
expectedValid: true,
},
{
name: "IPv4 with port in chain",
header: "10.0.0.1:45678, 192.168.8.39:38792, 127.0.0.1",
expectedIP: "192.168.8.39",
expectedValid: true,
},
// Invalid cases
{
name: "Invalid IP",
header: "invalid-ip",
expectedIP: "",
expectedValid: false,
},
{
name: "Empty header",
header: "",
expectedIP: "",
expectedValid: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ip, valid := engine.validateHeader(tc.header)
assert.Equal(t, tc.expectedValid, valid)
assert.Equal(t, tc.expectedIP, ip)
})
}
}
// TestNormalizeIPFromHeader tests the normalizeIPFromHeader helper function.
func TestNormalizeIPFromHeader(t *testing.T) {
testCases := []struct {
name string
input string
expectedIP string
expectedOK bool
}{
// Standard formats
{
name: "IPv4 plain",
input: "192.168.8.39",
expectedIP: "",
expectedOK: false, // net.ParseIP succeeds on plain IPv4, so normalizeIPFromHeader isn't called
},
{
name: "IPv6 plain",
input: "240e:318:2f4a:de56::240",
expectedIP: "",
expectedOK: false, // net.ParseIP succeeds on plain IPv6, so normalizeIPFromHeader isn't called
},
// Non-standard formats that need normalization
{
name: "IPv6 with square brackets",
input: "[240e:318:2f4a:de56::240]",
expectedIP: "240e:318:2f4a:de56::240",
expectedOK: true,
},
{
name: "IPv4 with port",
input: "192.168.8.39:38792",
expectedIP: "192.168.8.39",
expectedOK: true,
},
{
name: "IPv6 with square brackets and port",
input: "[240e:318:2f4a:de56::240]:38792",
expectedIP: "240e:318:2f4a:de56::240",
expectedOK: true,
},
{
name: "IPv6 localhost with brackets and port",
input: "[::1]:1234",
expectedIP: "::1",
expectedOK: true,
},
// Invalid cases
{
name: "Invalid IP",
input: "invalid-ip",
expectedIP: "",
expectedOK: false,
},
{
name: "Empty string",
input: "",
expectedIP: "",
expectedOK: false,
},
{
name: "Just brackets",
input: "[]",
expectedIP: "",
expectedOK: false,
},
{
name: "IPv4 with invalid port",
input: "192.168.8.39:abc",
expectedIP: "",
expectedOK: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ip, parsedIP := normalizeIPFromHeader(tc.input)
if tc.expectedOK {
assert.NotNil(t, parsedIP)
assert.Equal(t, tc.expectedIP, ip)
} else {
assert.Nil(t, parsedIP)
}
})
}
}
// TestClientIPWithNonStandardXForwardedFor tests that ClientIP correctly handles
// non-standard X-Forwarded-For header formats as described in issue #4572.
func TestClientIPWithNonStandardXForwardedFor(t *testing.T) {
SetMode(TestMode)
testCases := []struct {
name string
remoteAddr string
xForwardedFor string
trustedProxies []string
expectedIP string
}{
// IPv6 with square brackets
{
name: "IPv6 with square brackets in X-Forwarded-For",
remoteAddr: "127.0.0.1:1234",
xForwardedFor: "[240e:318:2f4a:de56::240]",
trustedProxies: []string{"127.0.0.1"},
expectedIP: "240e:318:2f4a:de56::240",
},
// IPv4 with port
{
name: "IPv4 with port in X-Forwarded-For",
remoteAddr: "127.0.0.1:1234",
xForwardedFor: "192.168.8.39:38792",
trustedProxies: []string{"127.0.0.1"},
expectedIP: "192.168.8.39",
},
// IPv6 with square brackets and port
{
name: "IPv6 with square brackets and port in X-Forwarded-For",
remoteAddr: "127.0.0.1:1234",
xForwardedFor: "[240e:318:2f4a:de56::240]:38792",
trustedProxies: []string{"127.0.0.1"},
expectedIP: "240e:318:2f4a:de56::240",
},
// Multiple entries with non-standard formats
{
name: "Chain with IPv6 with brackets and port",
remoteAddr: "127.0.0.1:1234",
xForwardedFor: "[2001:db8::1]:8080, [240e:318:2f4a:de56::240]:12345",
trustedProxies: []string{"127.0.0.1"},
expectedIP: "240e:318:2f4a:de56::240",
},
// Standard format should still work
{
name: "Standard IPv4",
remoteAddr: "127.0.0.1:1234",
xForwardedFor: "192.168.8.39",
trustedProxies: []string{"127.0.0.1"},
expectedIP: "192.168.8.39",
},
{
name: "Standard IPv6",
remoteAddr: "127.0.0.1:1234",
xForwardedFor: "240e:318:2f4a:de56::240",
trustedProxies: []string{"127.0.0.1"},
expectedIP: "240e:318:2f4a:de56::240",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/test", nil)
c.Request.Header.Set("X-Forwarded-For", tc.xForwardedFor)
c.Request.RemoteAddr = tc.remoteAddr
c.engine.ForwardedByClientIP = true
c.engine.RemoteIPHeaders = []string{"X-Forwarded-For"}
_ = c.engine.SetTrustedProxies(tc.trustedProxies)
assert.Equal(t, tc.expectedIP, c.ClientIP())
})
}
}