mirror of
https://github.com/gin-gonic/gin.git
synced 2026-04-29 23:23:18 +08:00
fix(engine): accept bracketed IPv6 and port-suffixed entries in X-Forwarded-For
`validateHeader` called `net.ParseIP` directly on each comma-split item, so anything with brackets or a `:port` suffix got rejected silently and `ClientIP()` fell through to `RemoteAddr` — which means a client coming in through IIS/ARR or certain cloud LBs would show up as the reverse proxy instead of the real caller. The four forms called out in #4572 are all normal real-world outputs: - "192.168.8.39" - "240e:318:2f4a:de56::240" - "[240e:318:2f4a:de56::240]" - "192.168.8.39:38792" - "[240e:318:2f4a:de56::240]:38792" Extract a small `parseForwardedForItem` helper that tries `net.SplitHostPort` first (handles the two `:port` variants and strips brackets in the process) and falls back to bracket-stripping + `net.ParseIP` for bare `[ipv6]`. The returned `clientIP` is now always the bare IP regardless of which proxy produced the header, which keeps the shape of `ClientIP()` stable. Table tests cover all four reporter-listed forms, plus a chain with a port on the last entry and a couple of garbage inputs. Closes #4572
This commit is contained in:
parent
d3ffc99852
commit
2dc3c02373
33
gin.go
33
gin.go
@ -485,8 +485,8 @@ func (engine *Engine) validateHeader(header string) (clientIP string, valid bool
|
|||||||
}
|
}
|
||||||
items := strings.Split(header, ",")
|
items := strings.Split(header, ",")
|
||||||
for i := len(items) - 1; i >= 0; i-- {
|
for i := len(items) - 1; i >= 0; i-- {
|
||||||
ipStr := strings.TrimSpace(items[i])
|
item := strings.TrimSpace(items[i])
|
||||||
ip := net.ParseIP(ipStr)
|
ipStr, ip := parseForwardedForItem(item)
|
||||||
if ip == nil {
|
if ip == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -500,6 +500,35 @@ func (engine *Engine) validateHeader(header string) (clientIP string, valid bool
|
|||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseForwardedForItem normalizes a single X-Forwarded-For entry and parses it.
|
||||||
|
// It accepts the four common forms emitted by reverse proxies:
|
||||||
|
//
|
||||||
|
// - "1.2.3.4"
|
||||||
|
// - "2001:db8::1"
|
||||||
|
// - "[2001:db8::1]" (IIS/ARR style)
|
||||||
|
// - "1.2.3.4:12345" (with port, some LBs)
|
||||||
|
// - "[2001:db8::1]:12345" (IIS/ARR + port)
|
||||||
|
//
|
||||||
|
// The returned string is the IP without brackets or port, so callers see a
|
||||||
|
// consistent form regardless of which proxy produced the header.
|
||||||
|
func parseForwardedForItem(item string) (string, net.IP) {
|
||||||
|
// Try host:port form first (handles "ip:port" and "[ipv6]:port").
|
||||||
|
if host, _, err := net.SplitHostPort(item); err == nil {
|
||||||
|
if ip := net.ParseIP(host); ip != nil {
|
||||||
|
return host, ip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Strip optional surrounding brackets for bare "[ipv6]" with no port.
|
||||||
|
unbracketed := item
|
||||||
|
if strings.HasPrefix(unbracketed, "[") && strings.HasSuffix(unbracketed, "]") {
|
||||||
|
unbracketed = unbracketed[1 : len(unbracketed)-1]
|
||||||
|
}
|
||||||
|
if ip := net.ParseIP(unbracketed); ip != nil {
|
||||||
|
return unbracketed, ip
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
|||||||
32
gin_test.go
32
gin_test.go
@ -1156,3 +1156,35 @@ func TestUpdateRouteTreesCalledOnce(t *testing.T) {
|
|||||||
assert.Equal(t, "ok", w.Body.String())
|
assert.Equal(t, "ok", w.Body.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateHeaderForwardedForForms(t *testing.T) {
|
||||||
|
engine := New()
|
||||||
|
// Disable trusted proxies so the rightmost parseable entry is returned.
|
||||||
|
require.NoError(t, engine.SetTrustedProxies(nil))
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
header string
|
||||||
|
wantIP string
|
||||||
|
wantOK bool
|
||||||
|
}{
|
||||||
|
{"plain IPv4", "192.168.8.39", "192.168.8.39", true},
|
||||||
|
{"plain IPv6", "240e:318:2f4a:de56::240", "240e:318:2f4a:de56::240", true},
|
||||||
|
{"bracketed IPv6 (IIS/ARR)", "[240e:318:2f4a:de56::240]", "240e:318:2f4a:de56::240", true},
|
||||||
|
{"IPv4 with port", "192.168.8.39:38792", "192.168.8.39", true},
|
||||||
|
{"bracketed IPv6 with port", "[240e:318:2f4a:de56::240]:38792", "240e:318:2f4a:de56::240", true},
|
||||||
|
{"IPv6 loopback bracketed", "[::1]", "::1", true},
|
||||||
|
{"chain with port on last entry", "1.2.3.4, 5.6.7.8:9000", "5.6.7.8", true},
|
||||||
|
{"empty", "", "", false},
|
||||||
|
{"garbage", "not-an-ip", "", false},
|
||||||
|
{"bracketed garbage", "[not-an-ip]", "", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
gotIP, gotOK := engine.validateHeader(tt.header)
|
||||||
|
assert.Equal(t, tt.wantOK, gotOK)
|
||||||
|
assert.Equal(t, tt.wantIP, gotIP)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user