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
42 changes: 42 additions & 0 deletions gin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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)
Expand Down
255 changes: 255 additions & 0 deletions gin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
})
}
}
Loading