🔥 Feature: Add TrustedProxies feature (#1397)

* 🔥 Feature: Add TrustedProxies feature

Add TrustedProxies feature to prevent HTTP header spoofing.

* Update ctx.go

* Update app.go

* Update app.go

Co-authored-by: hi019 <65871571+hi019@users.noreply.github.com>
pull/1425/head
Oleg Roshnivskyy 2021-06-30 09:03:45 +03:00 committed by GitHub
parent e082880297
commit 5ba78cd24a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 164 additions and 2 deletions

33
app.go
View File

@ -295,6 +295,34 @@ type Config struct {
// //
// Default: NetworkTCP4 // Default: NetworkTCP4
Network string Network string
// If you find yourself behind some sort of proxy, like a load balancer,
// then certain header information may be sent to you using special X-Forwarded-* headers or the Forwarded header.
// For example, the Host HTTP header is usually used to return the requested host.
// But when youre behind a proxy, the actual host may be stored in an X-Forwarded-Host header.
//
// If you are behind a proxy, you should enable TrustedProxyCheck to prevent header spoofing.
// If you enable EnableTrustedProxyCheck and leave TrustedProxies empty Fiber will skip
// all headers that could be spoofed.
// If request ip in TrustedProxies whitelist then:
// 1. c.Protocol() get value from X-Forwarded-Proto, X-Forwarded-Protocol, X-Forwarded-Ssl or X-Url-Scheme header
// 2. c.IP() get value from ProxyHeader header.
// 3. c.Hostname() get value from X-Forwarded-Host header
// But if request ip NOT in Trusted Proxies whitelist then:
// 1. c.Protocol() WON't get value from X-Forwarded-Proto, X-Forwarded-Protocol, X-Forwarded-Ssl or X-Url-Scheme header,
// will return https in case when tls connection is handled by the app, of http otherwise
// 2. c.IP() WON'T get value from ProxyHeader header, will return RemoteIP() from fasthttp context
// 3. c.Hostname() WON'T get value from X-Forwarded-Host header, fasthttp.Request.URI().Host()
// will be used to get the hostname.
//
// Default: false
EnableTrustedProxyCheck bool `json:"enable_trusted_proxy_check"`
// Read EnableTrustedProxyCheck doc.
//
// Default: []string
TrustedProxies []string `json:"trusted_proxies"`
trustedProxiesMap map[string]struct{}
} }
// Static defines configuration options when defining static assets. // Static defines configuration options when defining static assets.
@ -417,6 +445,11 @@ func New(config ...Config) *App {
app.config.Network = NetworkTCP4 app.config.Network = NetworkTCP4
} }
app.config.trustedProxiesMap = make(map[string]struct{}, len(app.config.TrustedProxies))
for _, ip := range app.config.TrustedProxies {
app.config.trustedProxiesMap[ip] = struct{}{}
}
// Init app // Init app
app.init() app.init()

25
ctx.go
View File

@ -526,18 +526,26 @@ func (c *Ctx) Get(key string, defaultValue ...string) string {
return defaultString(c.app.getString(c.fasthttp.Request.Header.Peek(key)), defaultValue) return defaultString(c.app.getString(c.fasthttp.Request.Header.Peek(key)), defaultValue)
} }
// Hostname contains the hostname derived from the Host HTTP header. // Hostname contains the hostname derived from the X-Forwarded-Host or Host HTTP header.
// Returned value is only valid within the handler. Do not store any references. // Returned value is only valid within the handler. Do not store any references.
// Make copies or use the Immutable setting instead. // Make copies or use the Immutable setting instead.
// Please use Config.EnableTrustedProxyCheck to prevent header spoofing, in case when your app is behind the proxy.
func (c *Ctx) Hostname() string { func (c *Ctx) Hostname() string {
if c.IsProxyTrusted() {
if host := c.Get(HeaderXForwardedHost); len(host) > 0 {
return host
}
}
return c.app.getString(c.fasthttp.Request.URI().Host()) return c.app.getString(c.fasthttp.Request.URI().Host())
} }
// IP returns the remote IP address of the request. // IP returns the remote IP address of the request.
// Please use Config.EnableTrustedProxyCheck to prevent header spoofing, in case when your app is behind the proxy.
func (c *Ctx) IP() string { func (c *Ctx) IP() string {
if len(c.app.config.ProxyHeader) > 0 { if c.IsProxyTrusted() && len(c.app.config.ProxyHeader) > 0 {
return c.Get(c.app.config.ProxyHeader) return c.Get(c.app.config.ProxyHeader)
} }
return c.fasthttp.RemoteIP().String() return c.fasthttp.RemoteIP().String()
} }
@ -739,11 +747,15 @@ func (c *Ctx) Path(override ...string) string {
} }
// Protocol contains the request protocol string: http or https for TLS requests. // Protocol contains the request protocol string: http or https for TLS requests.
// Use Config.EnableTrustedProxyCheck to prevent header spoofing, in case when your app is behind the proxy.
func (c *Ctx) Protocol() string { func (c *Ctx) Protocol() string {
if c.fasthttp.IsTLS() { if c.fasthttp.IsTLS() {
return "https" return "https"
} }
scheme := "http" scheme := "http"
if !c.IsProxyTrusted() {
return scheme
}
c.fasthttp.Request.Header.VisitAll(func(key, val []byte) { c.fasthttp.Request.Header.VisitAll(func(key, val []byte) {
if len(key) < 12 { if len(key) < 12 {
return // X-Forwarded- return // X-Forwarded-
@ -1195,3 +1207,12 @@ func (c *Ctx) configDependentPaths() {
c.treePath = c.detectionPath[:3] c.treePath = c.detectionPath[:3]
} }
} }
func (c *Ctx) IsProxyTrusted() bool {
if !c.app.config.EnableTrustedProxyCheck {
return true
}
_, trustProxy := c.app.config.trustedProxiesMap[c.fasthttp.RemoteIP().String()]
return trustProxy
}

View File

@ -825,6 +825,42 @@ func Test_Ctx_Hostname(t *testing.T) {
utils.AssertEqual(t, "google.com", c.Hostname()) utils.AssertEqual(t, "google.com", c.Hostname())
} }
// go test -run Test_Ctx_Hostname_Untrusted
func Test_Ctx_Hostname_UntrustedProxy(t *testing.T) {
t.Parallel()
// Don't trust any proxy
{
app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{}})
c := app.AcquireCtx(&fasthttp.RequestCtx{})
c.Request().SetRequestURI("http://google.com/test")
c.Request().Header.Set(HeaderXForwardedHost, "google1.com")
utils.AssertEqual(t, "google.com", c.Hostname())
app.ReleaseCtx(c)
}
// Trust to specific proxy list
{
app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.8.0.0", "0.8.0.1"}})
c := app.AcquireCtx(&fasthttp.RequestCtx{})
c.Request().SetRequestURI("http://google.com/test")
c.Request().Header.Set(HeaderXForwardedHost, "google1.com")
utils.AssertEqual(t, "google.com", c.Hostname())
app.ReleaseCtx(c)
}
}
// go test -run Test_Ctx_Hostname_Trusted
func Test_Ctx_Hostname_TrustedProxy(t *testing.T) {
t.Parallel()
{
app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.0.0.0", "0.8.0.1"}})
c := app.AcquireCtx(&fasthttp.RequestCtx{})
c.Request().SetRequestURI("http://google.com/test")
c.Request().Header.Set(HeaderXForwardedHost, "google1.com")
utils.AssertEqual(t, "google1.com", c.Hostname())
app.ReleaseCtx(c)
}
}
// go test -run Test_Ctx_IP // go test -run Test_Ctx_IP
func Test_Ctx_IP(t *testing.T) { func Test_Ctx_IP(t *testing.T) {
t.Parallel() t.Parallel()
@ -843,6 +879,26 @@ func Test_Ctx_IP_ProxyHeader(t *testing.T) {
utils.AssertEqual(t, "", c.IP()) utils.AssertEqual(t, "", c.IP())
} }
// go test -run Test_Ctx_IP_UntrustedProxy
func Test_Ctx_IP_UntrustedProxy(t *testing.T) {
t.Parallel()
app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.8.0.1"}, ProxyHeader: HeaderXForwardedFor})
c := app.AcquireCtx(&fasthttp.RequestCtx{})
c.Request().Header.Set(HeaderXForwardedFor, "0.0.0.1")
defer app.ReleaseCtx(c)
utils.AssertEqual(t, "0.0.0.0", c.IP())
}
// go test -run Test_Ctx_IP_TrustedProxy
func Test_Ctx_IP_TrustedProxy(t *testing.T) {
t.Parallel()
app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.0.0.0"}, ProxyHeader: HeaderXForwardedFor})
c := app.AcquireCtx(&fasthttp.RequestCtx{})
c.Request().Header.Set(HeaderXForwardedFor, "0.0.0.1")
defer app.ReleaseCtx(c)
utils.AssertEqual(t, "0.0.0.1", c.IP())
}
// go test -run Test_Ctx_IPs -parallel // go test -run Test_Ctx_IPs -parallel
func Test_Ctx_IPs(t *testing.T) { func Test_Ctx_IPs(t *testing.T) {
t.Parallel() t.Parallel()
@ -1169,6 +1225,58 @@ func Benchmark_Ctx_Protocol(b *testing.B) {
utils.AssertEqual(b, "http", res) utils.AssertEqual(b, "http", res)
} }
// go test -run Test_Ctx_Protocol_TrustedProxy
func Test_Ctx_Protocol_TrustedProxy(t *testing.T) {
t.Parallel()
app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.0.0.0"}})
c := app.AcquireCtx(&fasthttp.RequestCtx{})
defer app.ReleaseCtx(c)
c.Request().Header.Set(HeaderXForwardedProto, "https")
utils.AssertEqual(t, "https", c.Protocol())
c.Request().Header.Reset()
c.Request().Header.Set(HeaderXForwardedProtocol, "https")
utils.AssertEqual(t, "https", c.Protocol())
c.Request().Header.Reset()
c.Request().Header.Set(HeaderXForwardedSsl, "on")
utils.AssertEqual(t, "https", c.Protocol())
c.Request().Header.Reset()
c.Request().Header.Set(HeaderXUrlScheme, "https")
utils.AssertEqual(t, "https", c.Protocol())
c.Request().Header.Reset()
utils.AssertEqual(t, "http", c.Protocol())
}
// go test -run Test_Ctx_Protocol_UnTrustedProxy
func Test_Ctx_Protocol_UnTrustedProxy(t *testing.T) {
t.Parallel()
app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.8.0.1"}})
c := app.AcquireCtx(&fasthttp.RequestCtx{})
defer app.ReleaseCtx(c)
c.Request().Header.Set(HeaderXForwardedProto, "https")
utils.AssertEqual(t, "http", c.Protocol())
c.Request().Header.Reset()
c.Request().Header.Set(HeaderXForwardedProtocol, "https")
utils.AssertEqual(t, "http", c.Protocol())
c.Request().Header.Reset()
c.Request().Header.Set(HeaderXForwardedSsl, "on")
utils.AssertEqual(t, "http", c.Protocol())
c.Request().Header.Reset()
c.Request().Header.Set(HeaderXUrlScheme, "https")
utils.AssertEqual(t, "http", c.Protocol())
c.Request().Header.Reset()
utils.AssertEqual(t, "http", c.Protocol())
}
// go test -run Test_Ctx_Query // go test -run Test_Ctx_Query
func Test_Ctx_Query(t *testing.T) { func Test_Ctx_Query(t *testing.T) {
t.Parallel() t.Parallel()