mirror of
https://github.com/gin-gonic/gin.git
synced 2026-04-29 23:23:18 +08:00
fix: support non-standard X-Forwarded-For header formats (#4572)
Handle non-standard IP formats in X-Forwarded-For header that are commonly set by reverse proxies like IIS and cloud load balancers: - IPv4 with port: '192.168.8.39:38792' - IPv6 with brackets: '[240e:318:2f4a:de56::240]' - IPv6 with brackets and port: '[240e:318:2f4a:de56::240]:38792' Added parseNonstandardIP() helper function that uses net.SplitHostPort() and bracket stripping to extract the clean IP address before validation. Closes #4572
This commit is contained in:
parent
d3ffc99852
commit
7b53fa1bac
@ -2112,6 +2112,44 @@ func TestContextClientIP(t *testing.T) {
|
|||||||
assert.Empty(t, c.ClientIP())
|
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) {
|
func resetContextForClientIPTests(c *Context) {
|
||||||
c.Request.Header.Set("X-Real-IP", " 10.10.10.10 ")
|
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")
|
c.Request.Header.Set("X-Forwarded-For", " 20.20.20.20, 30.30.30.30")
|
||||||
|
|||||||
37
gin.go
37
gin.go
@ -487,9 +487,16 @@ func (engine *Engine) validateHeader(header string) (clientIP string, valid bool
|
|||||||
for i := len(items) - 1; i >= 0; i-- {
|
for i := len(items) - 1; i >= 0; i-- {
|
||||||
ipStr := strings.TrimSpace(items[i])
|
ipStr := strings.TrimSpace(items[i])
|
||||||
ip := net.ParseIP(ipStr)
|
ip := net.ParseIP(ipStr)
|
||||||
|
if ip == nil {
|
||||||
|
// 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 {
|
if ip == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// X-Forwarded-For is appended by proxy
|
// X-Forwarded-For is appended by proxy
|
||||||
// Check IPs in reverse order and stop when find untrusted proxy
|
// Check IPs in reverse order and stop when find untrusted proxy
|
||||||
@ -500,6 +507,36 @@ func (engine *Engine) validateHeader(header string) (clientIP string, valid bool
|
|||||||
return "", false
|
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
|
// updateRouteTree do update to the route tree recursively
|
||||||
func updateRouteTree(n *node) {
|
func updateRouteTree(n *node) {
|
||||||
n.path = strings.ReplaceAll(n.path, escapedColon, colon)
|
n.path = strings.ReplaceAll(n.path, escapedColon, colon)
|
||||||
|
|||||||
77
gin_test.go
77
gin_test.go
@ -1156,3 +1156,80 @@ func TestUpdateRouteTreesCalledOnce(t *testing.T) {
|
|||||||
assert.Equal(t, "ok", w.Body.String())
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user