Skip to content

fix(ip): strip IPv6 zone before parsing IPs in ExtractIPFromXFFHeader - #3180

Open
TheNetherWatcher wants to merge 2 commits into
labstack:masterfrom
TheNetherWatcher:fix/xff-ipv6-zone
Open

TheNetherWatcher wants to merge 2 commits into
labstack:masterfrom
TheNetherWatcher:fix/xff-ipv6-zone

Conversation

@TheNetherWatcher

Copy link
Copy Markdown

Fixes #3179

ExtractIPFromXFFHeader passed the peer address straight to net.ParseIP. For a link-local peer with a zone (fe80::1%eth0) that returns nil, so the loop gave up and returned the proxy address instead of the client IP from X-Forwarded-For.

Changes:

  • Add stripIPv6Zone and use it before net.ParseIP in the XFF loop. The returned value is unchanged when every hop is trusted (still the raw furthest entry).
  • Reuse the helper in isTrustedPeer (scheme.go), which already stripped the zone inline.
  • Two test cases in TestExtractIPFromXFFHeader: a zoned link-local peer with an external XFF entry, and an all-trusted zoned chain.

go test -race ./... and go vet pass. Both new cases fail on master.

@atharvaSharma17 atharvaSharma17 left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix addresses the reported IPv6 zone issue, but the current implementation introduces a couple of edge cases.

stripIPv6Zone() strips everything after % without validating whether the address is actually IPv6. This means malformed XFF entries like 127.0.0.2%garbage are now accepted instead of triggering the previous fallback behavior.

Additionally, with TrustLinkLocal(false), the same untrusted peer (fe80::1%eth0) produces different RealIP() values depending on whether an XFF header is present. This could lead to inconsistent IP-based rate limiting or access control.

Could we validate scoped IPv6 addresses more strictly, preserve the original direct peer address when it's untrusted, and add regression tests for these cases?

The overall approach looks good, but these edge cases should be addressed before merging. @aldas @vishr

Comment thread ip.go

// stripIPv6Zone removes an IPv6 zone, e.g. "fe80::1%eth0", which net.ParseIP rejects.
func stripIPv6Zone(host string) string {
if i := strings.IndexByte(host, '%'); i != -1 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The helper strips % suffixes from any address, including malformed IPv4 addresses such as 127.0.0.2%garbage. This allows previously invalid XFF entries to pass validation.

Comment thread ip.go
ips[i] = strings.TrimPrefix(ips[i], "[")
ips[i] = strings.TrimSuffix(ips[i], "]")
ip := net.ParseIP(ips[i])
ip := net.ParseIP(stripIPv6Zone(ips[i]))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With TrustLinkLocal(false), an untrusted zoned IPv6 peer returns fe80::1%eth0 without XFF but fe80::1 when XFF is present. This causes inconsistent RealIP() values for the same peer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ExtractIPFromXFFHeader ignores X-Forwarded-For when the peer address has an IPv6 zone

2 participants