Skip to content

Commit 124825e

Browse files
yeyisanaldas
andauthored
Bugfix/1834 Fix X-Real-IP bug (#2007)
* Fix incorrect return ip value for RealIpHeader * Improve test file to compare correct real IPs to each other and have better comments * Refactor ip extractor tests to be more readable (longer but readable) Co-authored-by: toimtoimtoim <[email protected]>
1 parent 27b404b commit 124825e

File tree

3 files changed

+711
-214
lines changed

3 files changed

+711
-214
lines changed

echo.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -214,9 +214,9 @@ const (
214214
HeaderXForwardedSsl = "X-Forwarded-Ssl"
215215
HeaderXUrlScheme = "X-Url-Scheme"
216216
HeaderXHTTPMethodOverride = "X-HTTP-Method-Override"
217-
HeaderXRealIP = "X-Real-IP"
218-
HeaderXRequestID = "X-Request-ID"
219-
HeaderXCorrelationID = "X-Correlation-ID"
217+
HeaderXRealIP = "X-Real-Ip"
218+
HeaderXRequestID = "X-Request-Id"
219+
HeaderXCorrelationID = "X-Correlation-Id"
220220
HeaderXRequestedWith = "X-Requested-With"
221221
HeaderServer = "Server"
222222
HeaderOrigin = "Origin"

ip.go

Lines changed: 134 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,130 @@ import (
66
"strings"
77
)
88

9+
/**
10+
By: https://github.com/tmshn (See: https://github.com/labstack/echo/pull/1478 , https://github.com/labstack/echox/pull/134 )
11+
Source: https://echo.labstack.com/guide/ip-address/
12+
13+
IP address plays fundamental role in HTTP; it's used for access control, auditing, geo-based access analysis and more.
14+
Echo provides handy method [`Context#RealIP()`](https://godoc.org/github.com/labstack/echo#Context) for that.
15+
16+
However, it is not trivial to retrieve the _real_ IP address from requests especially when you put L7 proxies before the application.
17+
In such situation, _real_ IP needs to be relayed on HTTP layer from proxies to your app, but you must not trust HTTP headers unconditionally.
18+
Otherwise, you might give someone a chance of deceiving you. **A security risk!**
19+
20+
To retrieve IP address reliably/securely, you must let your application be aware of the entire architecture of your infrastructure.
21+
In Echo, this can be done by configuring `Echo#IPExtractor` appropriately.
22+
This guides show you why and how.
23+
24+
> Note: if you dont' set `Echo#IPExtractor` explicitly, Echo fallback to legacy behavior, which is not a good choice.
25+
26+
Let's start from two questions to know the right direction:
27+
28+
1. Do you put any HTTP (L7) proxy in front of the application?
29+
- It includes both cloud solutions (such as AWS ALB or GCP HTTP LB) and OSS ones (such as Nginx, Envoy or Istio ingress gateway).
30+
2. If yes, what HTTP header do your proxies use to pass client IP to the application?
31+
32+
## Case 1. With no proxy
33+
34+
If you put no proxy (e.g.: directory facing to the internet), all you need to (and have to) see is IP address from network layer.
35+
Any HTTP header is untrustable because the clients have full control what headers to be set.
36+
37+
In this case, use `echo.ExtractIPDirect()`.
38+
39+
```go
40+
e.IPExtractor = echo.ExtractIPDirect()
41+
```
42+
43+
## Case 2. With proxies using `X-Forwarded-For` header
44+
45+
[`X-Forwared-For` (XFF)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For) is the popular header
46+
to relay clients' IP addresses.
47+
At each hop on the proxies, they append the request IP address at the end of the header.
48+
49+
Following example diagram illustrates this behavior.
50+
51+
```text
52+
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
53+
│ "Origin" │───────────>│ Proxy 1 │───────────>│ Proxy 2 │───────────>│ Your app │
54+
│ (IP: a) │ │ (IP: b) │ │ (IP: c) │ │ │
55+
└──────────┘ └──────────┘ └──────────┘ └──────────┘
56+
57+
Case 1.
58+
XFF: "" "a" "a, b"
59+
~~~~~~
60+
Case 2.
61+
XFF: "x" "x, a" "x, a, b"
62+
~~~~~~~~~
63+
↑ What your app will see
64+
```
65+
66+
In this case, use **first _untrustable_ IP reading from right**. Never use first one reading from left, as it is
67+
configurable by client. Here "trustable" means "you are sure the IP address belongs to your infrastructre".
68+
In above example, if `b` and `c` are trustable, the IP address of the client is `a` for both cases, never be `x`.
69+
70+
In Echo, use `ExtractIPFromXFFHeader(...TrustOption)`.
71+
72+
```go
73+
e.IPExtractor = echo.ExtractIPFromXFFHeader()
74+
```
75+
76+
By default, it trusts internal IP addresses (loopback, link-local unicast, private-use and unique local address
77+
from [RFC6890](https://tools.ietf.org/html/rfc6890), [RFC4291](https://tools.ietf.org/html/rfc4291) and
78+
[RFC4193](https://tools.ietf.org/html/rfc4193)).
79+
To control this behavior, use [`TrustOption`](https://godoc.org/github.com/labstack/echo#TrustOption)s.
80+
81+
E.g.:
82+
83+
```go
84+
e.IPExtractor = echo.ExtractIPFromXFFHeader(
85+
TrustLinkLocal(false),
86+
TrustIPRanges(lbIPRange),
87+
)
88+
```
89+
90+
- Ref: https://godoc.org/github.com/labstack/echo#TrustOption
91+
92+
## Case 3. With proxies using `X-Real-IP` header
93+
94+
`X-Real-IP` is another HTTP header to relay clients' IP addresses, but it carries only one address unlike XFF.
95+
96+
If your proxies set this header, use `ExtractIPFromRealIPHeader(...TrustOption)`.
97+
98+
```go
99+
e.IPExtractor = echo.ExtractIPFromRealIPHeader()
100+
```
101+
102+
Again, it trusts internal IP addresses by default (loopback, link-local unicast, private-use and unique local address
103+
from [RFC6890](https://tools.ietf.org/html/rfc6890), [RFC4291](https://tools.ietf.org/html/rfc4291) and
104+
[RFC4193](https://tools.ietf.org/html/rfc4193)).
105+
To control this behavior, use [`TrustOption`](https://godoc.org/github.com/labstack/echo#TrustOption)s.
106+
107+
- Ref: https://godoc.org/github.com/labstack/echo#TrustOption
108+
109+
> **Never forget** to configure the outermost proxy (i.e.; at the edge of your infrastructure) **not to pass through incoming headers**.
110+
> Otherwise there is a chance of fraud, as it is what clients can control.
111+
112+
## About default behavior
113+
114+
In default behavior, Echo sees all of first XFF header, X-Real-IP header and IP from network layer.
115+
116+
As you might already notice, after reading this article, this is not good.
117+
Sole reason this is default is just backward compatibility.
118+
119+
## Private IP ranges
120+
121+
See: https://en.wikipedia.org/wiki/Private_network
122+
123+
Private IPv4 address ranges (RFC 1918):
124+
* 10.0.0.0 – 10.255.255.255 (24-bit block)
125+
* 172.16.0.0 – 172.31.255.255 (20-bit block)
126+
* 192.168.0.0 – 192.168.255.255 (16-bit block)
127+
128+
Private IPv6 address ranges:
129+
* fc00::/7 address block = RFC 4193 Unique Local Addresses (ULA)
130+
131+
*/
132+
9133
type ipChecker struct {
10134
trustLoopback bool
11135
trustLinkLocal bool
@@ -52,6 +176,7 @@ func newIPChecker(configs []TrustOption) *ipChecker {
52176
return checker
53177
}
54178

179+
// Go1.16+ added `ip.IsPrivate()` but until that use this implementation
55180
func isPrivateIPRange(ip net.IP) bool {
56181
if ip4 := ip.To4(); ip4 != nil {
57182
return ip4[0] == 10 ||
@@ -87,25 +212,26 @@ type IPExtractor func(*http.Request) string
87212
// ExtractIPDirect extracts IP address using actual IP address.
88213
// Use this if your server faces to internet directory (i.e.: uses no proxy).
89214
func ExtractIPDirect() IPExtractor {
90-
return func(req *http.Request) string {
91-
ra, _, _ := net.SplitHostPort(req.RemoteAddr)
92-
return ra
93-
}
215+
return extractIP
216+
}
217+
218+
func extractIP(req *http.Request) string {
219+
ra, _, _ := net.SplitHostPort(req.RemoteAddr)
220+
return ra
94221
}
95222

96223
// ExtractIPFromRealIPHeader extracts IP address using x-real-ip header.
97224
// Use this if you put proxy which uses this header.
98225
func ExtractIPFromRealIPHeader(options ...TrustOption) IPExtractor {
99226
checker := newIPChecker(options)
100227
return func(req *http.Request) string {
101-
directIP := ExtractIPDirect()(req)
102228
realIP := req.Header.Get(HeaderXRealIP)
103229
if realIP != "" {
104-
if ip := net.ParseIP(directIP); ip != nil && checker.trust(ip) {
230+
if ip := net.ParseIP(realIP); ip != nil && checker.trust(ip) {
105231
return realIP
106232
}
107233
}
108-
return directIP
234+
return extractIP(req)
109235
}
110236
}
111237

@@ -115,7 +241,7 @@ func ExtractIPFromRealIPHeader(options ...TrustOption) IPExtractor {
115241
func ExtractIPFromXFFHeader(options ...TrustOption) IPExtractor {
116242
checker := newIPChecker(options)
117243
return func(req *http.Request) string {
118-
directIP := ExtractIPDirect()(req)
244+
directIP := extractIP(req)
119245
xffs := req.Header[HeaderXForwardedFor]
120246
if len(xffs) == 0 {
121247
return directIP

0 commit comments

Comments
 (0)