Skip to content

Commit

Permalink
🔥Feature: Add support for TrustProxy (#3170)
Browse files Browse the repository at this point in the history
* 🔥 Feature: Add `TrustProxyConfig` and rename `EnableTrustedProxyCheck` to `TrustProxy`

* 📚 Doc: Document TrustProxyConfig usage and migration

* 🚨 Test: Validate and Benchmark use of TrustProxyConfig

* 🩹 Fix: typo in RequestMethods docstring

* 🩹 Fix: typos in TrustProxy docstring and JSON tags

* 🩹 Fix: Move `TrustProxyConfig.Loopback` to beginning of if-statement

* 🎨 Style: Cleanup spacing for Test_Ctx_IsProxyTrusted

* 📚 Doc: Replace `whitelist` with `allowlist` for clarity

* 📚 Doc: Improve `TrustProxy` doc wording

* 🩹 Fix: validate IP addresses in `App.handleTrustedProxy`

* 🩹 Fix: grammatical errors and capitalize "TLS"
  • Loading branch information
xEricL authored Oct 17, 2024
1 parent 7b3a36f commit 298975a
Show file tree
Hide file tree
Showing 12 changed files with 510 additions and 167 deletions.
6 changes: 4 additions & 2 deletions .github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -561,8 +561,10 @@ import (

func main() {
app := fiber.New(fiber.Config{
EnableTrustedProxyCheck: true,
TrustedProxies: []string{"0.0.0.0", "1.1.1.1/30"}, // IP address or IP address range
TrustProxy: true,
TrustProxyConfig: fiber.TrustProxyConfig{
Proxies: []string{"0.0.0.0", "1.1.1.1/30"}, // IP address or IP address range
},
ProxyHeader: fiber.HeaderXForwardedFor,
})

Expand Down
73 changes: 55 additions & 18 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,29 +330,31 @@ type Config struct { //nolint:govet // Aligning the struct fields is not necessa
// For example, the Host HTTP header is usually used to return the requested host.
// But when you’re 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
// If you are behind a proxy, you should enable TrustProxy to prevent header spoofing.
// If you enable TrustProxy and do not provide a TrustProxyConfig, Fiber will skip
// all headers that could be spoofed.
// If request ip in TrustedProxies whitelist then:
// If the request IP is in the TrustProxyConfig.Proxies allowlist, then:
// 1. c.Scheme() 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.Host() and c.Hostname() get value from X-Forwarded-Host header
// But if request ip NOT in Trusted Proxies whitelist then:
// 1. c.Scheme() 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
// But if the request IP is NOT in the TrustProxyConfig.Proxies allowlist, then:
// 1. c.Scheme() WON'T get value from X-Forwarded-Proto, X-Forwarded-Protocol, X-Forwarded-Ssl or X-Url-Scheme header,
// will return https when a TLS connection is handled by the app, or http otherwise.
// 2. c.IP() WON'T get value from ProxyHeader header, will return RemoteIP() from fasthttp context
// 3. c.Host() and c.Hostname() WON'T get value from X-Forwarded-Host header, fasthttp.Request.URI().Host()
// will be used to get the hostname.
//
// To automatically trust all loopback, link-local, or private IP addresses,
// without manually adding them to the TrustProxyConfig.Proxies allowlist,
// you can set TrustProxyConfig.Loopback, TrustProxyConfig.LinkLocal, or TrustProxyConfig.Private to true.
//
// Default: false
EnableTrustedProxyCheck bool `json:"enable_trusted_proxy_check"`
TrustProxy bool `json:"trust_proxy"`

// Read EnableTrustedProxyCheck doc.
// Read TrustProxy doc.
//
// Default: []string
TrustedProxies []string `json:"trusted_proxies"`
trustedProxiesMap map[string]struct{}
trustedProxyRanges []*net.IPNet
// Default: DefaultTrustProxyConfig
TrustProxyConfig TrustProxyConfig `json:"trust_proxy_config"`

// If set to true, c.IP() and c.IPs() will validate IP addresses before returning them.
// Also, c.IP() will return only the first valid IP rather than just the raw header
Expand All @@ -372,7 +374,7 @@ type Config struct { //nolint:govet // Aligning the struct fields is not necessa
// Default: nil
StructValidator StructValidator

// RequestMethods provides customizibility for HTTP methods. You can add/remove methods as you wish.
// RequestMethods provides customizability for HTTP methods. You can add/remove methods as you wish.
//
// Optional. Default: DefaultMethods
RequestMethods []string
Expand All @@ -385,6 +387,36 @@ type Config struct { //nolint:govet // Aligning the struct fields is not necessa
EnableSplittingOnParsers bool `json:"enable_splitting_on_parsers"`
}

// Default TrustProxyConfig
var DefaultTrustProxyConfig = TrustProxyConfig{}

// TrustProxyConfig is a struct for configuring trusted proxies if Config.TrustProxy is true.
type TrustProxyConfig struct {
ips map[string]struct{}

// Proxies is a list of trusted proxy IP addresses or CIDR ranges.
//
// Default: []string
Proxies []string `json:"proxies"`

ranges []*net.IPNet

// LinkLocal enables trusting all link-local IP ranges (e.g., 169.254.0.0/16, fe80::/10).
//
// Default: false
LinkLocal bool `json:"link_local"`

// Loopback enables trusting all loopback IP ranges (e.g., 127.0.0.0/8, ::1/128).
//
// Default: false
Loopback bool `json:"loopback"`

// Private enables trusting all private IP ranges (e.g., 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7).
//
// Default: false
Private bool `json:"private"`
}

// RouteMessage is some message need to be print when server starts
type RouteMessage struct {
name string
Expand Down Expand Up @@ -510,8 +542,8 @@ func New(config ...Config) *App {
app.config.RequestMethods = DefaultMethods
}

app.config.trustedProxiesMap = make(map[string]struct{}, len(app.config.TrustedProxies))
for _, ipAddress := range app.config.TrustedProxies {
app.config.TrustProxyConfig.ips = make(map[string]struct{}, len(app.config.TrustProxyConfig.Proxies))
for _, ipAddress := range app.config.TrustProxyConfig.Proxies {
app.handleTrustedProxy(ipAddress)
}

Expand All @@ -529,17 +561,22 @@ func New(config ...Config) *App {
return app
}

// Adds an ip address to trustedProxyRanges or trustedProxiesMap based on whether it is an IP range or not
// Adds an ip address to TrustProxyConfig.ranges or TrustProxyConfig.ips based on whether it is an IP range or not
func (app *App) handleTrustedProxy(ipAddress string) {
if strings.Contains(ipAddress, "/") {
_, ipNet, err := net.ParseCIDR(ipAddress)
if err != nil {
log.Warnf("IP range %q could not be parsed: %v", ipAddress, err)
} else {
app.config.trustedProxyRanges = append(app.config.trustedProxyRanges, ipNet)
app.config.TrustProxyConfig.ranges = append(app.config.TrustProxyConfig.ranges, ipNet)
}
} else {
app.config.trustedProxiesMap[ipAddress] = struct{}{}
ip := net.ParseIP(ipAddress)
if ip == nil {
log.Warnf("IP address %q could not be parsed", ipAddress)
} else {
app.config.TrustProxyConfig.ips[ipAddress] = struct{}{}
}
}
}

Expand Down
24 changes: 15 additions & 9 deletions ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ type TLSHandler struct {

// GetClientInfo Callback function to set ClientHelloInfo
// Must comply with the method structure of https://cs.opensource.google/go/go/+/refs/tags/go1.20:src/crypto/tls/common.go;l=554-563
// Since we overlay the method of the tls config in the listener method
// Since we overlay the method of the TLS config in the listener method
func (t *TLSHandler) GetClientInfo(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
t.clientHelloInfo = info
return nil, nil //nolint:nilnil // Not returning anything useful here is probably fine
Expand Down Expand Up @@ -684,7 +684,7 @@ func (c *DefaultCtx) GetReqHeaders() map[string][]string {
// while `Hostname` refers specifically to the name assigned to a device on a network, excluding any port information.
// Example: URL: https://example.com:8080 -> Host: example.com:8080
// 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.
// Please use Config.TrustProxy to prevent header spoofing, in case when your app is behind the proxy.
func (c *DefaultCtx) Host() string {
if c.IsProxyTrusted() {
if host := c.Get(HeaderXForwardedHost); len(host) > 0 {
Expand All @@ -702,7 +702,7 @@ func (c *DefaultCtx) Host() string {
// Returned value is only valid within the handler. Do not store any references.
// Example: URL: https://example.com:8080 -> Hostname: example.com
// 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.
// Please use Config.TrustProxy to prevent header spoofing, in case when your app is behind the proxy.
func (c *DefaultCtx) Hostname() string {
addr, _ := parseAddr(c.Host())

Expand All @@ -720,7 +720,7 @@ func (c *DefaultCtx) Port() string {

// IP returns the remote IP address of the request.
// If ProxyHeader and IP Validation is configured, it will parse that header and return the first valid IP address.
// Please use Config.EnableTrustedProxyCheck to prevent header spoofing, in case when your app is behind the proxy.
// Please use Config.TrustProxy to prevent header spoofing, in case when your app is behind the proxy.
func (c *DefaultCtx) IP() string {
if c.IsProxyTrusted() && len(c.app.config.ProxyHeader) > 0 {
return c.extractIPFromHeader(c.app.config.ProxyHeader)
Expand Down Expand Up @@ -1116,7 +1116,7 @@ func (c *DefaultCtx) Path(override ...string) string {
}

// Scheme contains the request protocol string: http or https for TLS requests.
// Please use Config.EnableTrustedProxyCheck to prevent header spoofing, in case when your app is behind the proxy.
// Please use Config.TrustProxy to prevent header spoofing, in case when your app is behind the proxy.
func (c *DefaultCtx) Scheme() string {
if c.fasthttp.IsTLS() {
return schemeHTTPS
Expand Down Expand Up @@ -1819,20 +1819,26 @@ func (c *DefaultCtx) configDependentPaths() {
}

// IsProxyTrusted checks trustworthiness of remote ip.
// If EnableTrustedProxyCheck false, it returns true
// If Config.TrustProxy false, it returns true
// IsProxyTrusted can check remote ip by proxy ranges and ip map.
func (c *DefaultCtx) IsProxyTrusted() bool {
if !c.app.config.EnableTrustedProxyCheck {
if !c.app.config.TrustProxy {
return true
}

ip := c.fasthttp.RemoteIP()

if _, trusted := c.app.config.trustedProxiesMap[ip.String()]; trusted {
if (c.app.config.TrustProxyConfig.Loopback && ip.IsLoopback()) ||
(c.app.config.TrustProxyConfig.Private && ip.IsPrivate()) ||
(c.app.config.TrustProxyConfig.LinkLocal && ip.IsLinkLocalUnicast()) {
return true
}

for _, ipNet := range c.app.config.trustedProxyRanges {
if _, trusted := c.app.config.TrustProxyConfig.ips[ip.String()]; trusted {
return true
}

for _, ipNet := range c.app.config.TrustProxyConfig.ranges {
if ipNet.Contains(ip) {
return true
}
Expand Down
10 changes: 5 additions & 5 deletions ctx_interface_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

1 comment on commit 298975a

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.50.

Benchmark suite Current: 298975a Previous: 8c84b0f Ratio
Benchmark_Ctx_Send 7.126 ns/op 0 B/op 0 allocs/op 4.335 ns/op 0 B/op 0 allocs/op 1.64
Benchmark_Ctx_Send - ns/op 7.126 ns/op 4.335 ns/op 1.64
Benchmark_Utils_GetOffer/1_parameter 201.2 ns/op 0 B/op 0 allocs/op 131 ns/op 0 B/op 0 allocs/op 1.54
Benchmark_Utils_GetOffer/1_parameter - ns/op 201.2 ns/op 131 ns/op 1.54
Benchmark_Middleware_BasicAuth - B/op 80 B/op 48 B/op 1.67
Benchmark_Middleware_BasicAuth - allocs/op 5 allocs/op 3 allocs/op 1.67
Benchmark_Middleware_BasicAuth_Upper - B/op 80 B/op 48 B/op 1.67
Benchmark_Middleware_BasicAuth_Upper - allocs/op 5 allocs/op 3 allocs/op 1.67
Benchmark_CORS_NewHandler - B/op 16 B/op 0 B/op +∞
Benchmark_CORS_NewHandler - allocs/op 1 allocs/op 0 allocs/op +∞
Benchmark_CORS_NewHandlerSingleOrigin - B/op 16 B/op 0 B/op +∞
Benchmark_CORS_NewHandlerSingleOrigin - allocs/op 1 allocs/op 0 allocs/op +∞
Benchmark_CORS_NewHandlerPreflight - B/op 104 B/op 0 B/op +∞
Benchmark_CORS_NewHandlerPreflight - allocs/op 5 allocs/op 0 allocs/op +∞
Benchmark_CORS_NewHandlerPreflightSingleOrigin - B/op 104 B/op 0 B/op +∞
Benchmark_CORS_NewHandlerPreflightSingleOrigin - allocs/op 5 allocs/op 0 allocs/op +∞
Benchmark_CORS_NewHandlerPreflightWildcard - B/op 104 B/op 0 B/op +∞
Benchmark_CORS_NewHandlerPreflightWildcard - allocs/op 5 allocs/op 0 allocs/op +∞
Benchmark_Middleware_CSRF_GenerateToken - B/op 519 B/op 327 B/op 1.59
Benchmark_Middleware_CSRF_GenerateToken - allocs/op 10 allocs/op 6 allocs/op 1.67

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.