Skip to content

Commit

Permalink
Safer/trustable extraction of real ip from request
Browse files Browse the repository at this point in the history
  • Loading branch information
tmshn committed Jan 20, 2020
1 parent 399da56 commit 44976af
Show file tree
Hide file tree
Showing 5 changed files with 293 additions and 12 deletions.
10 changes: 1 addition & 9 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"fmt"
"io"
"mime/multipart"
"net"
"net/http"
"net/url"
"os"
Expand Down Expand Up @@ -270,14 +269,7 @@ func (c *context) Scheme() string {
}

func (c *context) RealIP() string {
if ip := c.request.Header.Get(HeaderXForwardedFor); ip != "" {
return strings.Split(ip, ", ")[0]
}
if ip := c.request.Header.Get(HeaderXRealIP); ip != "" {
return ip
}
ra, _, _ := net.SplitHostPort(c.request.RemoteAddr)
return ra
return c.echo.IPExtracter(c.request)
}

func (c *context) Path() string {
Expand Down
4 changes: 4 additions & 0 deletions context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -814,12 +814,14 @@ func TestContext_Logger(t *testing.T) {
}

func TestContext_RealIP(t *testing.T) {
e := New()
tests := []struct {
c Context
s string
}{
{
&context{
echo: e,
request: &http.Request{
Header: http.Header{HeaderXForwardedFor: []string{"127.0.0.1, 127.0.1.1, "}},
},
Expand All @@ -828,6 +830,7 @@ func TestContext_RealIP(t *testing.T) {
},
{
&context{
echo: e,
request: &http.Request{
Header: http.Header{
"X-Real-Ip": []string{"192.168.0.1"},
Expand All @@ -838,6 +841,7 @@ func TestContext_RealIP(t *testing.T) {
},
{
&context{
echo: e,
request: &http.Request{
RemoteAddr: "89.89.89.89:1654",
},
Expand Down
8 changes: 5 additions & 3 deletions echo.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ type (
Validator Validator
Renderer Renderer
Logger Logger
IPExtracter IPExtracter
}

// Route contains a handler and information for matching against requests.
Expand Down Expand Up @@ -298,9 +299,10 @@ func New() (e *Echo) {
AutoTLSManager: autocert.Manager{
Prompt: autocert.AcceptTOS,
},
Logger: log.New("echo"),
colorer: color.New(),
maxParam: new(int),
Logger: log.New("echo"),
colorer: color.New(),
maxParam: new(int),
IPExtracter: ExtractIPLegacy,
}
e.Server.Handler = e
e.TLSServer.Handler = e
Expand Down
144 changes: 144 additions & 0 deletions ip.go
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]
}
}
139 changes: 139 additions & 0 deletions ip_test.go
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)
}
}
}

0 comments on commit 44976af

Please sign in to comment.