Skip to content

Commit 7c5af01

Browse files
authored
Safer/trustable extraction of real ip from request (#1478)
* Safer/trustable extraction of real ip from request * Fix x-real-ip handling on proxy * fix docs * fix default check
1 parent 5ddc3a6 commit 7c5af01

File tree

6 files changed

+425
-1
lines changed

6 files changed

+425
-1
lines changed

context.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ type (
4343

4444
// RealIP returns the client's network address based on `X-Forwarded-For`
4545
// or `X-Real-IP` request header.
46+
// The behavior can be configured using `Echo#IPExtractor`.
4647
RealIP() string
4748

4849
// Path returns the registered path for the handler.
@@ -270,6 +271,10 @@ func (c *context) Scheme() string {
270271
}
271272

272273
func (c *context) RealIP() string {
274+
if c.echo != nil && c.echo.IPExtractor != nil {
275+
return c.echo.IPExtractor(c.request)
276+
}
277+
// Fall back to legacy behavior
273278
if ip := c.request.Header.Get(HeaderXForwardedFor); ip != "" {
274279
return strings.Split(ip, ", ")[0]
275280
}

echo.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ type (
9090
Validator Validator
9191
Renderer Renderer
9292
Logger Logger
93+
IPExtractor IPExtractor
9394
}
9495

9596
// Route contains a handler and information for matching against requests.

ip.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package echo
2+
3+
import (
4+
"net"
5+
"net/http"
6+
"strings"
7+
)
8+
9+
type ipChecker struct {
10+
trustLoopback bool
11+
trustLinkLocal bool
12+
trustPrivateNet bool
13+
trustExtraRanges []*net.IPNet
14+
}
15+
16+
// TrustOption is config for which IP address to trust
17+
type TrustOption func(*ipChecker)
18+
19+
// TrustLoopback configures if you trust loopback address (default: true).
20+
func TrustLoopback(v bool) TrustOption {
21+
return func(c *ipChecker) {
22+
c.trustLoopback = v
23+
}
24+
}
25+
26+
// TrustLinkLocal configures if you trust link-local address (default: true).
27+
func TrustLinkLocal(v bool) TrustOption {
28+
return func(c *ipChecker) {
29+
c.trustLinkLocal = v
30+
}
31+
}
32+
33+
// TrustPrivateNet configures if you trust private network address (default: true).
34+
func TrustPrivateNet(v bool) TrustOption {
35+
return func(c *ipChecker) {
36+
c.trustPrivateNet = v
37+
}
38+
}
39+
40+
// TrustIPRange add trustable IP ranges using CIDR notation.
41+
func TrustIPRange(ipRange *net.IPNet) TrustOption {
42+
return func(c *ipChecker) {
43+
c.trustExtraRanges = append(c.trustExtraRanges, ipRange)
44+
}
45+
}
46+
47+
func newIPChecker(configs []TrustOption) *ipChecker {
48+
checker := &ipChecker{trustLoopback: true, trustLinkLocal: true, trustPrivateNet: true}
49+
for _, configure := range configs {
50+
configure(checker)
51+
}
52+
return checker
53+
}
54+
55+
func isPrivateIPRange(ip net.IP) bool {
56+
if ip4 := ip.To4(); ip4 != nil {
57+
return ip4[0] == 10 ||
58+
ip4[0] == 172 && ip4[1]&0xf0 == 16 ||
59+
ip4[0] == 192 && ip4[1] == 168
60+
}
61+
return len(ip) == net.IPv6len && ip[0]&0xfe == 0xfc
62+
}
63+
64+
func (c *ipChecker) trust(ip net.IP) bool {
65+
if c.trustLoopback && ip.IsLoopback() {
66+
return true
67+
}
68+
if c.trustLinkLocal && ip.IsLinkLocalUnicast() {
69+
return true
70+
}
71+
if c.trustPrivateNet && isPrivateIPRange(ip) {
72+
return true
73+
}
74+
for _, trustedRange := range c.trustExtraRanges {
75+
if trustedRange.Contains(ip) {
76+
return true
77+
}
78+
}
79+
return false
80+
}
81+
82+
// IPExtractor is a function to extract IP addr from http.Request.
83+
// Set appropriate one to Echo#IPExtractor.
84+
// See https://echo.labstack.com/guide/ip-address for more details.
85+
type IPExtractor func(*http.Request) string
86+
87+
// ExtractIPDirect extracts IP address using actual IP address.
88+
// Use this if your server faces to internet directory (i.e.: uses no proxy).
89+
func ExtractIPDirect() IPExtractor {
90+
return func(req *http.Request) string {
91+
ra, _, _ := net.SplitHostPort(req.RemoteAddr)
92+
return ra
93+
}
94+
}
95+
96+
// ExtractIPFromRealIPHeader extracts IP address using x-real-ip header.
97+
// Use this if you put proxy which uses this header.
98+
func ExtractIPFromRealIPHeader(options ...TrustOption) IPExtractor {
99+
checker := newIPChecker(options)
100+
return func(req *http.Request) string {
101+
directIP := ExtractIPDirect()(req)
102+
realIP := req.Header.Get(HeaderXRealIP)
103+
if realIP != "" {
104+
if ip := net.ParseIP(directIP); ip != nil && checker.trust(ip) {
105+
return realIP
106+
}
107+
}
108+
return directIP
109+
}
110+
}
111+
112+
// ExtractIPFromXFFHeader extracts IP address using x-forwarded-for header.
113+
// Use this if you put proxy which uses this header.
114+
// This returns nearest untrustable IP. If all IPs are trustable, returns furthest one (i.e.: XFF[0]).
115+
func ExtractIPFromXFFHeader(options ...TrustOption) IPExtractor {
116+
checker := newIPChecker(options)
117+
return func(req *http.Request) string {
118+
directIP := ExtractIPDirect()(req)
119+
xffs := req.Header[HeaderXForwardedFor]
120+
if len(xffs) == 0 {
121+
return directIP
122+
}
123+
ips := append(strings.Split(strings.Join(xffs, ","), ","), directIP)
124+
for i := len(ips) - 1; i >= 0; i-- {
125+
ip := net.ParseIP(strings.TrimSpace(ips[i]))
126+
if ip == nil {
127+
// Unable to parse IP; cannot trust entire records
128+
return directIP
129+
}
130+
if !checker.trust(ip) {
131+
return ip.String()
132+
}
133+
}
134+
// All of the IPs are trusted; return first element because it is furthest from server (best effort strategy).
135+
return strings.TrimSpace(ips[0])
136+
}
137+
}

0 commit comments

Comments
 (0)