mirror of
https://github.com/gin-gonic/gin.git
synced 2025-10-18 06:42:10 +08:00
X-Forwarded-For handling is unsafe
Breaks API, but immensely improves security Fixes #2473
This commit is contained in:
parent
b94d23d1b4
commit
de0f9eb338
37
README.md
37
README.md
@ -2115,6 +2115,43 @@ func main() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Don't trust all proxies
|
||||||
|
|
||||||
|
Gin lets you specify which headers to hold the real client IP (if any),
|
||||||
|
as well as specifying which proxies (or direct clients) you trust to
|
||||||
|
specify one of these headers.
|
||||||
|
|
||||||
|
The `TrustedProxies` slice on your `gin.Engine` specifes the clients
|
||||||
|
truest to specify unspoofed client IP headers. Proxies can be specified
|
||||||
|
as IP's, CIDR's, or hostnames. Hostnames are resolved on each query,
|
||||||
|
such that changes in your proxy pool take effect immediately. The
|
||||||
|
hostname option is handy, but also costly, so only use if you have no
|
||||||
|
other option.
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
router := gin.Default()
|
||||||
|
router.TrustedProxies = []string{"192.168.1.2"}
|
||||||
|
|
||||||
|
router.GET("/", func(c *gin.Context) {
|
||||||
|
|
||||||
|
// If the client is 192.168.1.2, use the X-Forwarded-For
|
||||||
|
// header to deduce the original client IP from the trust-
|
||||||
|
// worthy parts of that header.
|
||||||
|
// Otherwise, simply return the direct client IP
|
||||||
|
fmt.Printf("ClientIP: %s\n", c.ClientIP())
|
||||||
|
})
|
||||||
|
|
||||||
|
router.Run()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
|
91
context.go
91
context.go
@ -713,14 +713,12 @@ func (c *Context) ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (e
|
|||||||
// X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy.
|
// X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy.
|
||||||
// Use X-Forwarded-For before X-Real-Ip as nginx uses X-Real-Ip with the proxy's IP.
|
// Use X-Forwarded-For before X-Real-Ip as nginx uses X-Real-Ip with the proxy's IP.
|
||||||
func (c *Context) ClientIP() string {
|
func (c *Context) ClientIP() string {
|
||||||
if c.engine.ForwardedByClientIP {
|
if c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil {
|
||||||
clientIP := c.requestHeader("X-Forwarded-For")
|
for _, header := range c.engine.RemoteIPHeaders {
|
||||||
clientIP = strings.TrimSpace(strings.Split(clientIP, ",")[0])
|
ipChain := filterIPsFromUntrustedProxies(c.requestHeader(header), c.Request, c.engine)
|
||||||
if clientIP == "" {
|
if len(ipChain) > 0 {
|
||||||
clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip"))
|
return ipChain[0]
|
||||||
}
|
}
|
||||||
if clientIP != "" {
|
|
||||||
return clientIP
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -730,11 +728,82 @@ func (c *Context) ClientIP() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr)); err == nil {
|
ip, _ := getTransportPeerIPForRequest(c.Request)
|
||||||
return ip
|
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterIPsFromUntrustedProxies(XForwardedForHeader string, req *http.Request, e *Engine) []string {
|
||||||
|
var items, out []string
|
||||||
|
if XForwardedForHeader != "" {
|
||||||
|
items = strings.Split(XForwardedForHeader, ",")
|
||||||
|
} else {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
if peerIP, err := getTransportPeerIPForRequest(req); err == nil {
|
||||||
|
items = append(items, peerIP)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ""
|
for i := len(items) - 1; i >= 0; i-- {
|
||||||
|
item := strings.TrimSpace(items[i])
|
||||||
|
ip := net.ParseIP(item)
|
||||||
|
if ip == nil {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
out = prependString(ip.String(), out)
|
||||||
|
if !isTrustedProxy(ip, e) {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
// out = prependString(ip.String(), out)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func isTrustedProxy(ip net.IP, e *Engine) bool {
|
||||||
|
for _, trustedProxy := range e.TrustedProxies {
|
||||||
|
if _, ipnet, err := net.ParseCIDR(trustedProxy); err == nil {
|
||||||
|
if ipnet.Contains(ip) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if proxyIP := net.ParseIP(trustedProxy); proxyIP != nil {
|
||||||
|
if proxyIP.Equal(ip) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if addrs, err := e.lookupHost(trustedProxy); err == nil {
|
||||||
|
for _, proxyAddr := range addrs {
|
||||||
|
proxyIP := net.ParseIP(proxyAddr)
|
||||||
|
if proxyIP == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if proxyIP.Equal(ip) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func prependString(ip string, ipList []string) []string {
|
||||||
|
ipList = append(ipList, "")
|
||||||
|
copy(ipList[1:], ipList)
|
||||||
|
ipList[0] = string(ip)
|
||||||
|
return ipList
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTransportPeerIPForRequest(req *http.Request) (string, error) {
|
||||||
|
var err error
|
||||||
|
if ip, _, err := net.SplitHostPort(strings.TrimSpace(req.RemoteAddr)); err == nil {
|
||||||
|
return ip, nil
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContentType returns the Content-Type header of the request.
|
// ContentType returns the Content-Type header of the request.
|
||||||
|
@ -1380,11 +1380,23 @@ func TestContextClientIP(t *testing.T) {
|
|||||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
c.Request, _ = http.NewRequest("POST", "/", nil)
|
c.Request, _ = http.NewRequest("POST", "/", nil)
|
||||||
|
|
||||||
c.Request.Header.Set("X-Real-IP", " 10.10.10.10 ")
|
c.engine.lookupHost = func(host string) ([]string, error) {
|
||||||
c.Request.Header.Set("X-Forwarded-For", " 20.20.20.20, 30.30.30.30")
|
if host == "foo" {
|
||||||
c.Request.Header.Set("X-Appengine-Remote-Addr", "50.50.50.50")
|
return []string{"40.40.40.40", "30.30.30.30"}, nil
|
||||||
c.Request.RemoteAddr = " 40.40.40.40:42123 "
|
}
|
||||||
|
if host == "bar" {
|
||||||
|
return nil, errors.New("hostname lookup failed")
|
||||||
|
}
|
||||||
|
if host == "baz" {
|
||||||
|
return []string{"thisshouldneverhappen"}, nil
|
||||||
|
}
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resetContextForClientIPTests(c)
|
||||||
|
|
||||||
|
// Legacy tests (validating that the defaults don't break the
|
||||||
|
// (insecure!) old behaviour)
|
||||||
assert.Equal(t, "20.20.20.20", c.ClientIP())
|
assert.Equal(t, "20.20.20.20", c.ClientIP())
|
||||||
|
|
||||||
c.Request.Header.Del("X-Forwarded-For")
|
c.Request.Header.Del("X-Forwarded-For")
|
||||||
@ -1404,6 +1416,74 @@ func TestContextClientIP(t *testing.T) {
|
|||||||
// no port
|
// no port
|
||||||
c.Request.RemoteAddr = "50.50.50.50"
|
c.Request.RemoteAddr = "50.50.50.50"
|
||||||
assert.Empty(t, c.ClientIP())
|
assert.Empty(t, c.ClientIP())
|
||||||
|
|
||||||
|
// Tests exercising the TrustedProxies functionality
|
||||||
|
resetContextForClientIPTests(c)
|
||||||
|
|
||||||
|
// No trusted proxies
|
||||||
|
c.engine.TrustedProxies = []string{}
|
||||||
|
c.engine.RemoteIPHeaders = []string{"X-Forwarded-For"}
|
||||||
|
assert.Equal(t, "40.40.40.40", c.ClientIP())
|
||||||
|
|
||||||
|
// Last proxy is trusted, but the RemoteAddr is not
|
||||||
|
c.engine.TrustedProxies = []string{"30.30.30.30"}
|
||||||
|
assert.Equal(t, "40.40.40.40", c.ClientIP())
|
||||||
|
|
||||||
|
// Only trust RemoteAddr
|
||||||
|
c.engine.TrustedProxies = []string{"40.40.40.40"}
|
||||||
|
assert.Equal(t, "30.30.30.30", c.ClientIP())
|
||||||
|
|
||||||
|
// All steps are trusted
|
||||||
|
c.engine.TrustedProxies = []string{"40.40.40.40", "30.30.30.30", "20.20.20.20"}
|
||||||
|
assert.Equal(t, "20.20.20.20", c.ClientIP())
|
||||||
|
|
||||||
|
// Use CIDR
|
||||||
|
c.engine.TrustedProxies = []string{"40.40.25.25/16", "30.30.30.30"}
|
||||||
|
assert.Equal(t, "20.20.20.20", c.ClientIP())
|
||||||
|
|
||||||
|
// Use hostname that resolves to all the proxies
|
||||||
|
c.engine.TrustedProxies = []string{"foo"}
|
||||||
|
assert.Equal(t, "20.20.20.20", c.ClientIP())
|
||||||
|
|
||||||
|
// Use hostname that returns an error
|
||||||
|
c.engine.TrustedProxies = []string{"bar"}
|
||||||
|
assert.Equal(t, "40.40.40.40", c.ClientIP())
|
||||||
|
|
||||||
|
// X-Forwarded-For has a non-IP element
|
||||||
|
c.engine.TrustedProxies = []string{"40.40.40.40"}
|
||||||
|
c.Request.Header.Set("X-Forwarded-For", " blah ")
|
||||||
|
assert.Equal(t, "40.40.40.40", c.ClientIP())
|
||||||
|
|
||||||
|
// Result from LookupHost has non-IP element. This should never
|
||||||
|
// happen, but we should test it to make sure we handle it
|
||||||
|
// gracefully.
|
||||||
|
c.engine.TrustedProxies = []string{"baz"}
|
||||||
|
c.Request.Header.Set("X-Forwarded-For", " 30.30.30.30 ")
|
||||||
|
assert.Equal(t, "40.40.40.40", c.ClientIP())
|
||||||
|
|
||||||
|
c.engine.TrustedProxies = []string{"40.40.40.40"}
|
||||||
|
c.Request.Header.Del("X-Forwarded-For")
|
||||||
|
c.engine.RemoteIPHeaders = []string{"X-Forwarded-For", "X-Real-IP"}
|
||||||
|
assert.Equal(t, "10.10.10.10", c.ClientIP())
|
||||||
|
|
||||||
|
c.engine.RemoteIPHeaders = []string{}
|
||||||
|
c.engine.AppEngine = true
|
||||||
|
assert.Equal(t, "50.50.50.50", c.ClientIP())
|
||||||
|
|
||||||
|
c.Request.Header.Del("X-Appengine-Remote-Addr")
|
||||||
|
assert.Equal(t, "40.40.40.40", c.ClientIP())
|
||||||
|
|
||||||
|
// no port
|
||||||
|
c.Request.RemoteAddr = "50.50.50.50"
|
||||||
|
assert.Empty(t, c.ClientIP())
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetContextForClientIPTests(c *Context) {
|
||||||
|
c.Request.Header.Set("X-Real-IP", " 10.10.10.10 ")
|
||||||
|
c.Request.Header.Set("X-Forwarded-For", " 20.20.20.20, 30.30.30.30")
|
||||||
|
c.Request.Header.Set("X-Appengine-Remote-Addr", "50.50.50.50")
|
||||||
|
c.Request.RemoteAddr = " 40.40.40.40:42123 "
|
||||||
|
c.engine.AppEngine = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContextContentType(t *testing.T) {
|
func TestContextContentType(t *testing.T) {
|
||||||
|
7
gin.go
7
gin.go
@ -82,6 +82,8 @@ type Engine struct {
|
|||||||
// handler.
|
// handler.
|
||||||
HandleMethodNotAllowed bool
|
HandleMethodNotAllowed bool
|
||||||
ForwardedByClientIP bool
|
ForwardedByClientIP bool
|
||||||
|
RemoteIPHeaders []string
|
||||||
|
TrustedProxies []string
|
||||||
|
|
||||||
// #726 #755 If enabled, it will thrust some headers starting with
|
// #726 #755 If enabled, it will thrust some headers starting with
|
||||||
// 'X-AppEngine...' for better integration with that PaaS.
|
// 'X-AppEngine...' for better integration with that PaaS.
|
||||||
@ -103,6 +105,8 @@ type Engine struct {
|
|||||||
// See the PR #1817 and issue #1644
|
// See the PR #1817 and issue #1644
|
||||||
RemoveExtraSlash bool
|
RemoveExtraSlash bool
|
||||||
|
|
||||||
|
lookupHost func(string) ([]string, error)
|
||||||
|
|
||||||
delims render.Delims
|
delims render.Delims
|
||||||
secureJSONPrefix string
|
secureJSONPrefix string
|
||||||
HTMLRender render.HTMLRender
|
HTMLRender render.HTMLRender
|
||||||
@ -139,11 +143,14 @@ func New() *Engine {
|
|||||||
RedirectFixedPath: false,
|
RedirectFixedPath: false,
|
||||||
HandleMethodNotAllowed: false,
|
HandleMethodNotAllowed: false,
|
||||||
ForwardedByClientIP: true,
|
ForwardedByClientIP: true,
|
||||||
|
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
|
||||||
|
TrustedProxies: []string{"0.0.0.0/0"},
|
||||||
AppEngine: defaultAppEngine,
|
AppEngine: defaultAppEngine,
|
||||||
UseRawPath: false,
|
UseRawPath: false,
|
||||||
RemoveExtraSlash: false,
|
RemoveExtraSlash: false,
|
||||||
UnescapePathValues: true,
|
UnescapePathValues: true,
|
||||||
MaxMultipartMemory: defaultMultipartMemory,
|
MaxMultipartMemory: defaultMultipartMemory,
|
||||||
|
lookupHost: net.LookupHost,
|
||||||
trees: make(methodTrees, 0, 9),
|
trees: make(methodTrees, 0, 9),
|
||||||
delims: render.Delims{Left: "{{", Right: "}}"},
|
delims: render.Delims{Left: "{{", Right: "}}"},
|
||||||
secureJSONPrefix: "while(1);",
|
secureJSONPrefix: "while(1);",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user