-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Safer/trustable extraction of real ip from request
- Loading branch information
Showing
5 changed files
with
293 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
package echo | ||
|
||
import ( | ||
"net" | ||
"net/http" | ||
"strings" | ||
) | ||
|
||
// IPExtracter is a function to extract IP addr from http.Request | ||
type IPExtracter func(*http.Request) string | ||
|
||
// ExtractIPDirect extracts IP address using actual IP address. | ||
// Use this if your server faces to internet directory (i.e.: uses no proxy). | ||
func ExtractIPDirect(req *http.Request) string { | ||
ra, _, _ := net.SplitHostPort(req.RemoteAddr) | ||
return ra | ||
} | ||
|
||
// ExtractIPFromRealIPHeader extracts IP address using x-real-ip header. | ||
// Use this if you put proxy which uses this header. | ||
func ExtractIPFromRealIPHeader(req *http.Request) string { | ||
if ip := req.Header.Get(HeaderXRealIP); ip != "" { | ||
return ip | ||
} | ||
return ExtractIPDirect(req) | ||
} | ||
|
||
// ExtractIPLegacy extracts IP using first XFF header, x-real-ip header and actual IP, in that order. | ||
// This is default behavior so that no breaking changes to happen, but using this might allow attackers to deceive you a wrong IP address. | ||
// Consider using anther extracter that fits your infrastructure architecture. | ||
func ExtractIPLegacy(req *http.Request) string { | ||
if xff := req.Header.Get(HeaderXForwardedFor); xff != "" { | ||
return strings.Split(xff, ", ")[0] | ||
} | ||
return ExtractIPFromRealIPHeader(req) | ||
} | ||
|
||
type ipChecker struct { | ||
trustLoopback bool | ||
trustLinkLocal bool | ||
trustPrivateNet bool | ||
trustExtraRanges []*net.IPNet | ||
trustExtraNProxies int | ||
} | ||
|
||
// TrustConfig is config for which IP address to trust (used for ExtractIPFromXFFHeader) | ||
type TrustConfig func(*ipChecker) | ||
|
||
// TrustLoopback configures if you trust loopback address (default: true). | ||
func TrustLoopback(v bool) TrustConfig { | ||
return func(c *ipChecker) { | ||
c.trustLoopback = v | ||
} | ||
} | ||
|
||
// TrustLinkLocal configures if you trust link-local address (default: true). | ||
func TrustLinkLocal(v bool) TrustConfig { | ||
return func(c *ipChecker) { | ||
c.trustLinkLocal = v | ||
} | ||
} | ||
|
||
// TrustPrivateNet configures if you trust private network address (default: true). | ||
func TrustPrivateNet(v bool) TrustConfig { | ||
return func(c *ipChecker) { | ||
c.trustPrivateNet = v | ||
} | ||
} | ||
|
||
// TrustIPRanges add trustable IP ranges using CIDR notation. | ||
func TrustIPRanges(ipRanges ...*net.IPNet) TrustConfig { | ||
return func(c *ipChecker) { | ||
c.trustExtraRanges = append(c.trustExtraRanges, ipRanges...) | ||
} | ||
} | ||
|
||
// TrustNProxies configures how many proxies you trust *after* all trustable IPs (default: 0). | ||
func TrustNProxies(n uint32) TrustConfig { | ||
return func(c *ipChecker) { | ||
c.trustExtraNProxies = int(n) | ||
} | ||
} | ||
|
||
var privateRanges []*net.IPNet = []*net.IPNet{ | ||
&net.IPNet{IP: net.IP{10, 0, 0, 0}, Mask: net.CIDRMask(8, 32)}, | ||
&net.IPNet{IP: net.IP{172, 16, 0, 0}, Mask: net.CIDRMask(12, 32)}, | ||
&net.IPNet{IP: net.IP{192, 168, 0, 0}, Mask: net.CIDRMask(16, 32)}, | ||
&net.IPNet{IP: net.IP{0xfc, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, Mask: net.CIDRMask(7, 128)}, | ||
} | ||
|
||
func (c *ipChecker) trust(ip net.IP) bool { | ||
if c.trustLoopback && ip.IsLoopback() { | ||
return true | ||
} | ||
if c.trustLinkLocal && ip.IsLinkLocalUnicast() { | ||
return true | ||
} | ||
if c.trustPrivateNet { | ||
for _, privateRange := range privateRanges { | ||
if privateRange.Contains(ip) { | ||
return true | ||
} | ||
} | ||
} | ||
for _, trustedRange := range c.trustExtraRanges { | ||
if trustedRange.Contains(ip) { | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
// ExtractIPFromXFFHeader extracts IP address using x-forwarded-for header. | ||
// Use this if you put proxy which uses this header. | ||
// This returns nearest untrustable IP, if available. Otherwise, returns furthest trustable IP. | ||
func ExtractIPFromXFFHeader(configs ...TrustConfig) IPExtracter { | ||
checker := &ipChecker{trustLoopback: true, trustLinkLocal: true, trustPrivateNet: true} | ||
for _, configure := range configs { | ||
configure(checker) | ||
} | ||
return func(req *http.Request) string { | ||
remoteAddr := ExtractIPDirect(req) | ||
xff := req.Header.Get(HeaderXForwardedFor) | ||
if xff == "" { | ||
return remoteAddr | ||
} | ||
ips := append(strings.Split(xff, ", "), remoteAddr) | ||
for i := len(ips) - 1; i >= 0; i-- { | ||
ip := net.ParseIP(ips[i]) | ||
if ip == nil { | ||
// This XFF header is broken; fallback to actual remote addr. | ||
return remoteAddr | ||
} | ||
if !checker.trust(ip) { | ||
if i < checker.trustExtraNProxies { | ||
break | ||
} | ||
return ips[i-checker.trustExtraNProxies] | ||
} | ||
} | ||
// All of the IPs are trusted; return first element because it is furthest from server (best effort strategy). | ||
return ips[0] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
package echo | ||
|
||
import ( | ||
"net" | ||
"net/http" | ||
"strings" | ||
"testing" | ||
|
||
testify "github.com/stretchr/testify/assert" | ||
) | ||
|
||
const ( | ||
// For RemoteAddr | ||
ipForRemoteAddrLoopback = "127.0.0.1" // From 127.0.0.0/8 | ||
sampleRemoteAddrLoopback = ipForRemoteAddrLoopback + ":8080" | ||
ipForRemoteAddrExternal = "203.0.113.1" | ||
sampleRemoteAddrExternal = ipForRemoteAddrExternal + ":8080" | ||
// For x-real-ip | ||
ipForRealIP = "203.0.113.10" | ||
// For XFF | ||
ipForXFF1LinkLocal = "169.254.0.101" // From 169.254.0.0/16 | ||
ipForXFF2Private = "192.168.0.102" // From 192.168.0.0/16 | ||
ipForXFF3External = "2001:db8::103" | ||
ipForXFF4Private = "fc00::104" // From fc00::/7 | ||
ipForXFF5External = "198.51.100.105" | ||
ipForXFF6External = "192.0.2.106" | ||
ipForXFFBroken = "this.is.broken.lol" | ||
) | ||
|
||
var ( | ||
sampleXFF = strings.Join([]string{ | ||
ipForXFF6External, ipForXFF5External, ipForXFF4Private, ipForXFF3External, ipForXFF2Private, ipForXFF1LinkLocal, | ||
}, ", ") | ||
|
||
requests = []*http.Request{ | ||
&http.Request{ | ||
RemoteAddr: sampleRemoteAddrExternal, | ||
}, | ||
&http.Request{ | ||
Header: http.Header{ | ||
"X-Real-Ip": []string{ipForRealIP}, | ||
}, | ||
RemoteAddr: sampleRemoteAddrExternal, | ||
}, | ||
&http.Request{ | ||
Header: http.Header{ | ||
"X-Real-Ip": []string{ipForRealIP}, | ||
HeaderXForwardedFor: []string{sampleXFF}, | ||
}, | ||
RemoteAddr: sampleRemoteAddrExternal, | ||
}, | ||
&http.Request{ | ||
Header: http.Header{ | ||
HeaderXForwardedFor: []string{sampleXFF}, | ||
}, | ||
RemoteAddr: sampleRemoteAddrExternal, | ||
}, | ||
&http.Request{ | ||
Header: http.Header{ | ||
HeaderXForwardedFor: []string{sampleXFF}, | ||
}, | ||
RemoteAddr: sampleRemoteAddrLoopback, | ||
}, | ||
&http.Request{ | ||
Header: http.Header{ | ||
HeaderXForwardedFor: []string{ipForXFFBroken + ", " + ipForXFF1LinkLocal}, | ||
}, | ||
RemoteAddr: sampleRemoteAddrLoopback, | ||
}, | ||
} | ||
) | ||
|
||
func TestExtractIP(t *testing.T) { | ||
_, ipv4AllRange, _ := net.ParseCIDR("0.0.0.0/0") | ||
_, ipv6AllRange, _ := net.ParseCIDR("::/0") | ||
_, ipForXFF3ExternalRange, _ := net.ParseCIDR(ipForXFF3External + "/48") | ||
_, ipForRemoteAddrExternalRange, _ := net.ParseCIDR(ipForRemoteAddrExternal + "/24") | ||
|
||
assert := testify.New(t) | ||
tests := map[string]*struct { | ||
extractor IPExtracter | ||
expectedIPs []string | ||
}{ | ||
"ExtractIPLegacy": { | ||
ExtractIPLegacy, | ||
[]string{ipForRemoteAddrExternal, ipForRealIP, ipForXFF6External, ipForXFF6External, ipForXFF6External, ipForXFFBroken}, | ||
}, | ||
"ExtractIPDirect": { | ||
ExtractIPDirect, | ||
[]string{ipForRemoteAddrExternal, ipForRemoteAddrExternal, ipForRemoteAddrExternal, ipForRemoteAddrExternal, ipForRemoteAddrLoopback, ipForRemoteAddrLoopback}, | ||
}, | ||
"ExtractIPFromRealIPHeader": { | ||
ExtractIPFromRealIPHeader, | ||
[]string{ipForRemoteAddrExternal, ipForRealIP, ipForRealIP, ipForRemoteAddrExternal, ipForRemoteAddrLoopback, ipForRemoteAddrLoopback}, | ||
}, | ||
"ExtractIPFromXFFHeader(default)": { | ||
ExtractIPFromXFFHeader(), | ||
[]string{ipForRemoteAddrExternal, ipForRemoteAddrExternal, ipForRemoteAddrExternal, ipForRemoteAddrExternal, ipForXFF3External, ipForRemoteAddrLoopback}, | ||
}, | ||
"ExtractIPFromXFFHeader(trust nothing)": { | ||
// This behaves equivalent to ExtractIPDirect | ||
ExtractIPFromXFFHeader(TrustLoopback(false), TrustLinkLocal(false), TrustPrivateNet(false)), | ||
[]string{ipForRemoteAddrExternal, ipForRemoteAddrExternal, ipForRemoteAddrExternal, ipForRemoteAddrExternal, ipForRemoteAddrLoopback, ipForRemoteAddrLoopback}, | ||
}, | ||
"ExtractIPFromXFFHeader(trust only direct-facing proxy)": { | ||
ExtractIPFromXFFHeader(TrustLoopback(false), TrustLinkLocal(false), TrustPrivateNet(false), TrustNProxies(1)), | ||
[]string{ipForRemoteAddrExternal, ipForRemoteAddrExternal, ipForXFF1LinkLocal, ipForXFF1LinkLocal, ipForXFF1LinkLocal, ipForXFF1LinkLocal}, | ||
}, | ||
"ExtractIPFromXFFHeader(trust direct-facing proxy)": { | ||
ExtractIPFromXFFHeader(TrustIPRanges(ipForRemoteAddrExternalRange)), | ||
[]string{ipForRemoteAddrExternal, ipForRemoteAddrExternal, ipForXFF3External, ipForXFF3External, ipForXFF3External, ipForRemoteAddrLoopback}, | ||
}, | ||
"ExtractIPFromXFFHeader(trust everything)": { | ||
// This behaves similar to ExtractIPLegacy, but ignores x-real-ip header. | ||
ExtractIPFromXFFHeader(TrustIPRanges(ipv4AllRange, ipv6AllRange)), | ||
[]string{ipForRemoteAddrExternal, ipForRemoteAddrExternal, ipForXFF6External, ipForXFF6External, ipForXFF6External, ipForRemoteAddrLoopback}, | ||
}, | ||
"ExtractIPFromXFFHeader(trust single proxy)": { | ||
ExtractIPFromXFFHeader(TrustNProxies(1)), | ||
[]string{ipForRemoteAddrExternal, ipForRemoteAddrExternal, ipForXFF1LinkLocal, ipForXFF1LinkLocal, ipForXFF4Private, ipForRemoteAddrLoopback}, | ||
}, | ||
"ExtractIPFromXFFHeader(trust ipForXFF3External)": { | ||
// This trusts private network also after "additional" trust ranges unlike `TrustNProxies(1)` doesn't | ||
ExtractIPFromXFFHeader(TrustIPRanges(ipForXFF3ExternalRange)), | ||
[]string{ipForRemoteAddrExternal, ipForRemoteAddrExternal, ipForRemoteAddrExternal, ipForRemoteAddrExternal, ipForXFF5External, ipForRemoteAddrLoopback}, | ||
}, | ||
"ExtractIPFromXFFHeader(trust too many proxies)": { | ||
ExtractIPFromXFFHeader(TrustNProxies(99)), | ||
[]string{ipForRemoteAddrExternal, ipForRemoteAddrExternal, ipForXFF6External, ipForXFF6External, ipForXFF6External, ipForRemoteAddrLoopback}, | ||
}, | ||
} | ||
for name, test := range tests { | ||
for i, req := range requests { | ||
actual := test.extractor(req) | ||
expected := test.expectedIPs[i] | ||
assert.Equal(expected, actual, "[%s] request #%d", name, i) | ||
} | ||
} | ||
} |