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
20 changes: 19 additions & 1 deletion ip.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package echo
import (
"net"
"net/http"
"net/netip"
"strings"
)

Expand Down Expand Up @@ -219,6 +220,19 @@ func extractIP(req *http.Request) string {
return host
}

// stripIPv6Zone removes the zone from a scoped IPv6 address, e.g. "fe80::1%eth0", which net.ParseIP rejects.
// Anything else, including an IPv4 address with a "%" suffix, is returned unchanged so that parsing still fails.
func stripIPv6Zone(host string) string {
if strings.IndexByte(host, '%') == -1 {
return host
}
addr, err := netip.ParseAddr(host)
if err != nil || addr.Zone() == "" {
return host
}
return addr.WithZone("").String()
}

// ExtractIPFromRealIPHeader extracts IP address using `x-real-ip` header.
// Use this if you put proxy which uses this header.
func ExtractIPFromRealIPHeader(options ...TrustOption) IPExtractor {
Expand Down Expand Up @@ -252,12 +266,16 @@ func ExtractIPFromXFFHeader(options ...TrustOption) IPExtractor {
ips[i] = strings.TrimSpace(ips[i])
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.

if ip == nil {
// Unable to parse IP; cannot trust entire records
return directIP
}
if !checker.trust(ip) {
if i == len(ips)-1 {
// Untrusted direct peer; return it as-is, as when there is no XFF header
return directIP
}
return ip.String()
}
}
Expand Down
43 changes: 43 additions & 0 deletions ip_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,49 @@ func TestExtractIPFromXFFHeader(t *testing.T) {
},
expectIP: "fe80::3",
},
{
name: "request from link-local proxy with IPv6 zone in remote addr, extract IP from XFF header",
whenRequest: http.Request{
Header: http.Header{
HeaderXForwardedFor: []string{"203.0.113.5"},
},
RemoteAddr: "[fe80::1%eth0]:8080",
},
expectIP: "203.0.113.5",
},
{
name: "request trusts all IPs in XFF header with IPv6 zones, extract IP from furthest in XFF chain",
whenRequest: http.Request{
Header: http.Header{
HeaderXForwardedFor: []string{"[fe80::2%eth0], [fe80::1%eth0]"},
},
RemoteAddr: "[fe80::1%eth0]:8080",
},
expectIP: "fe80::2%eth0",
},
{
name: "request from untrusted link-local peer with IPv6 zone in remote addr, extract IP from remote addr",
givenTrustOptions: []TrustOption{
TrustLinkLocal(false),
},
whenRequest: http.Request{
Header: http.Header{
HeaderXForwardedFor: []string{"203.0.113.5"},
},
RemoteAddr: "[fe80::1%eth0]:8080",
},
expectIP: "fe80::1%eth0",
},
{
name: "request has IPv4 address with zone-like suffix in XFF header, extract IP from remote addr",
whenRequest: http.Request{
Header: http.Header{
HeaderXForwardedFor: []string{"127.0.0.2%garbage"},
},
RemoteAddr: "127.0.0.1:8080",
},
expectIP: "127.0.0.1",
},
{
name: "request is from external IP has valid + UNTRUSTED external XFF header, extract IP from remote addr",
whenRequest: http.Request{
Expand Down
5 changes: 1 addition & 4 deletions scheme.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,7 @@ func isTrustedPeer(remoteAddr string, checker *ipChecker) bool {
if err != nil {
host = remoteAddr
}
if i := strings.IndexByte(host, '%'); i != -1 { // IPv6 zone, e.g. "fe80::1%eth0"
host = host[:i]
}
ip := net.ParseIP(host)
ip := net.ParseIP(stripIPv6Zone(host))
return ip != nil && checker.trust(ip)
}

Expand Down