浏览代码

httpd/webdav: allow to configure trusted proxy header and depth

Signed-off-by: Nicola Murino <[email protected]>
Nicola Murino 3 年之前
父节点
当前提交
f6b11c2d01
共有 11 个文件被更改,包括 172 次插入71 次删除
  1. 64 16
      config/config.go
  2. 16 0
      config/config_test.go
  3. 7 3
      docs/full-configuration.md
  4. 9 2
      httpd/httpd.go
  5. 6 5
      httpd/internal_test.go
  6. 1 1
      httpd/server.go
  7. 5 1
      sftpgo.json
  8. 20 22
      util/util.go
  9. 33 17
      webdavd/internal_test.go
  10. 1 1
      webdavd/server.go
  11. 10 3
      webdavd/webdavd.go

+ 64 - 16
config/config.go

@@ -67,16 +67,18 @@ var (
 		Debug:                      false,
 	}
 	defaultWebDAVDBinding = webdavd.Binding{
-		Address:            "",
-		Port:               0,
-		EnableHTTPS:        false,
-		CertificateFile:    "",
-		CertificateKeyFile: "",
-		MinTLSVersion:      12,
-		ClientAuthType:     0,
-		TLSCipherSuites:    nil,
-		Prefix:             "",
-		ProxyAllowed:       nil,
+		Address:             "",
+		Port:                0,
+		EnableHTTPS:         false,
+		CertificateFile:     "",
+		CertificateKeyFile:  "",
+		MinTLSVersion:       12,
+		ClientAuthType:      0,
+		TLSCipherSuites:     nil,
+		Prefix:              "",
+		ProxyAllowed:        nil,
+		ClientIPProxyHeader: "",
+		ClientIPHeaderDepth: 0,
 	}
 	defaultHTTPDBinding = httpd.Binding{
 		Address:               "",
@@ -90,6 +92,8 @@ var (
 		ClientAuthType:        0,
 		TLSCipherSuites:       nil,
 		ProxyAllowed:          nil,
+		ClientIPProxyHeader:   "",
+		ClientIPHeaderDepth:   0,
 		HideLoginURL:          0,
 		RenderOpenAPI:         true,
 		WebClientIntegrations: nil,
@@ -1126,6 +1130,30 @@ func applyFTPDBindingFromEnv(idx int, isSet bool, binding ftpd.Binding) {
 	}
 }
 
+func getWebDAVDBindingProxyConfigsFromEnv(idx int, binding *webdavd.Binding) bool {
+	isSet := false
+
+	proxyAllowed, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__PROXY_ALLOWED", idx))
+	if ok {
+		binding.ProxyAllowed = proxyAllowed
+		isSet = true
+	}
+
+	clientIPProxyHeader, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__CLIENT_IP_PROXY_HEADER", idx))
+	if ok {
+		binding.ClientIPProxyHeader = clientIPProxyHeader
+		isSet = true
+	}
+
+	clientIPHeaderDepth, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__CLIENT_IP_HEADER_DEPTH", idx))
+	if ok {
+		binding.ClientIPHeaderDepth = int(clientIPHeaderDepth)
+		isSet = true
+	}
+
+	return isSet
+}
+
 func getWebDAVDBindingFromEnv(idx int) {
 	binding := webdavd.Binding{
 		MinTLSVersion: 12,
@@ -1184,9 +1212,7 @@ func getWebDAVDBindingFromEnv(idx int) {
 		isSet = true
 	}
 
-	proxyAllowed, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__PROXY_ALLOWED", idx))
-	if ok {
-		binding.ProxyAllowed = proxyAllowed
+	if getWebDAVDBindingProxyConfigsFromEnv(idx, &binding) {
 		isSet = true
 	}
 
@@ -1519,6 +1545,30 @@ func getHTTPDNestedObjectsFromEnv(idx int, binding *httpd.Binding) bool {
 	return isSet
 }
 
+func getHTTPDBindingProxyConfigsFromEnv(idx int, binding *httpd.Binding) bool {
+	isSet := false
+
+	proxyAllowed, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__PROXY_ALLOWED", idx))
+	if ok {
+		binding.ProxyAllowed = proxyAllowed
+		isSet = true
+	}
+
+	clientIPProxyHeader, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__CLIENT_IP_PROXY_HEADER", idx))
+	if ok {
+		binding.ClientIPProxyHeader = clientIPProxyHeader
+		isSet = true
+	}
+
+	clientIPHeaderDepth, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__CLIENT_IP_HEADER_DEPTH", idx))
+	if ok {
+		binding.ClientIPHeaderDepth = int(clientIPHeaderDepth)
+		isSet = true
+	}
+
+	return isSet
+}
+
 func getHTTPDBindingFromEnv(idx int) {
 	binding := getDefaultHTTPBinding(idx)
 	isSet := false
@@ -1589,9 +1639,7 @@ func getHTTPDBindingFromEnv(idx int) {
 		isSet = true
 	}
 
-	proxyAllowed, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__PROXY_ALLOWED", idx))
-	if ok {
-		binding.ProxyAllowed = proxyAllowed
+	if getHTTPDBindingProxyConfigsFromEnv(idx, &binding) {
 		isSet = true
 	}
 

+ 16 - 0
config/config_test.go

@@ -837,6 +837,8 @@ func TestWebDAVBindingsFromEnv(t *testing.T) {
 	os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__ENABLE_HTTPS", "0")
 	os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__TLS_CIPHER_SUITES", "TLS_RSA_WITH_AES_128_CBC_SHA ")
 	os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__PROXY_ALLOWED", "192.168.10.1")
+	os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__CLIENT_IP_PROXY_HEADER", "X-Forwarded-For")
+	os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__CLIENT_IP_HEADER_DEPTH", "2")
 	os.Setenv("SFTPGO_WEBDAVD__BINDINGS__2__ADDRESS", "127.0.1.1")
 	os.Setenv("SFTPGO_WEBDAVD__BINDINGS__2__PORT", "9000")
 	os.Setenv("SFTPGO_WEBDAVD__BINDINGS__2__ENABLE_HTTPS", "1")
@@ -852,6 +854,8 @@ func TestWebDAVBindingsFromEnv(t *testing.T) {
 		os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__ENABLE_HTTPS")
 		os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__TLS_CIPHER_SUITES")
 		os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__PROXY_ALLOWED")
+		os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__CLIENT_IP_PROXY_HEADER")
+		os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__CLIENT_IP_HEADER_DEPTH")
 		os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__ADDRESS")
 		os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__PORT")
 		os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__ENABLE_HTTPS")
@@ -873,6 +877,7 @@ func TestWebDAVBindingsFromEnv(t *testing.T) {
 	require.Equal(t, 12, bindings[0].MinTLSVersion)
 	require.Len(t, bindings[0].TLSCipherSuites, 0)
 	require.Empty(t, bindings[0].Prefix)
+	require.Equal(t, 0, bindings[0].ClientIPHeaderDepth)
 	require.Equal(t, 8000, bindings[1].Port)
 	require.Equal(t, "127.0.0.1", bindings[1].Address)
 	require.False(t, bindings[1].EnableHTTPS)
@@ -881,6 +886,8 @@ func TestWebDAVBindingsFromEnv(t *testing.T) {
 	require.Len(t, bindings[1].TLSCipherSuites, 1)
 	require.Equal(t, "TLS_RSA_WITH_AES_128_CBC_SHA", bindings[1].TLSCipherSuites[0])
 	require.Equal(t, "192.168.10.1", bindings[1].ProxyAllowed[0])
+	require.Equal(t, "X-Forwarded-For", bindings[1].ClientIPProxyHeader)
+	require.Equal(t, 2, bindings[1].ClientIPHeaderDepth)
 	require.Empty(t, bindings[1].Prefix)
 	require.Equal(t, 9000, bindings[2].Port)
 	require.Equal(t, "127.0.1.1", bindings[2].Address)
@@ -891,6 +898,7 @@ func TestWebDAVBindingsFromEnv(t *testing.T) {
 	require.Equal(t, "/dav2", bindings[2].Prefix)
 	require.Equal(t, "webdav.crt", bindings[2].CertificateFile)
 	require.Equal(t, "webdav.key", bindings[2].CertificateKeyFile)
+	require.Equal(t, 0, bindings[2].ClientIPHeaderDepth)
 }
 
 func TestHTTPDBindingsFromEnv(t *testing.T) {
@@ -917,6 +925,8 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_AUTH_TYPE", "1")
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__TLS_CIPHER_SUITES", " TLS_AES_256_GCM_SHA384 , TLS_CHACHA20_POLY1305_SHA256")
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__PROXY_ALLOWED", " 192.168.9.1 , 172.16.25.0/24")
+	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_IP_PROXY_HEADER", "X-Real-IP")
+	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_IP_HEADER_DEPTH", "2")
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__HIDE_LOGIN_URL", "3")
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__WEB_CLIENT_INTEGRATIONS__1__URL", "http://127.0.0.1/")
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__WEB_CLIENT_INTEGRATIONS__1__FILE_EXTENSIONS", ".pdf, .txt")
@@ -979,6 +989,8 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_AUTH_TYPE")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__TLS_CIPHER_SUITES")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__PROXY_ALLOWED")
+		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_IP_PROXY_HEADER")
+		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_IP_HEADER_DEPTH")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__HIDE_LOGIN_URL")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__WEB_CLIENT_INTEGRATIONS__1__URL")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__WEB_CLIENT_INTEGRATIONS__1__FILE_EXTENSIONS")
@@ -1038,6 +1050,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 	require.Equal(t, "TLS_AES_128_GCM_SHA256", bindings[0].TLSCipherSuites[0])
 	require.Equal(t, 0, bindings[0].HideLoginURL)
 	require.False(t, bindings[0].Security.Enabled)
+	require.Equal(t, 0, bindings[0].ClientIPHeaderDepth)
 	require.Equal(t, 8000, bindings[1].Port)
 	require.Equal(t, "127.0.0.1", bindings[1].Address)
 	require.False(t, bindings[1].EnableHTTPS)
@@ -1051,6 +1064,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 	require.False(t, bindings[1].Security.Enabled)
 	require.Equal(t, "Web Admin", bindings[1].Branding.WebAdmin.Name)
 	require.Equal(t, "WebClient", bindings[1].Branding.WebClient.ShortName)
+	require.Equal(t, 0, bindings[1].ClientIPHeaderDepth)
 	require.Equal(t, 9000, bindings[2].Port)
 	require.Equal(t, "127.0.1.1", bindings[2].Address)
 	require.True(t, bindings[2].EnableHTTPS)
@@ -1065,6 +1079,8 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 	require.Len(t, bindings[2].ProxyAllowed, 2)
 	require.Equal(t, "192.168.9.1", bindings[2].ProxyAllowed[0])
 	require.Equal(t, "172.16.25.0/24", bindings[2].ProxyAllowed[1])
+	require.Equal(t, "X-Real-IP", bindings[2].ClientIPProxyHeader)
+	require.Equal(t, 2, bindings[2].ClientIPHeaderDepth)
 	require.Equal(t, 3, bindings[2].HideLoginURL)
 	require.Len(t, bindings[2].WebClientIntegrations, 1)
 	require.Equal(t, "http://127.0.0.1/", bindings[2].WebClientIntegrations[0].URL)

+ 7 - 3
docs/full-configuration.md

@@ -179,7 +179,9 @@ The configuration file contains the following sections:
     - `client_auth_type`, integer. Set to `1` to require a client certificate and verify it. Set to `2` to request a client certificate during the TLS handshake and verify it if given, in this mode the client is allowed not to send a certificate. At least one certification authority must be defined in order to verify client certificates. If no certification authority is defined, this setting is ignored. Default: 0.
     - `tls_cipher_suites`, list of strings. List of supported cipher suites for TLS version 1.2. If empty, a default list of secure cipher suites is used, with a preference order based on hardware performance. Note that TLS 1.3 ciphersuites are not configurable. The supported ciphersuites names are defined [here](https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L52). Any invalid name will be silently ignored. The order matters, the ciphers listed first will be the preferred ones. Default: empty.
     - `prefix`, string. Prefix for WebDAV resources, if empty WebDAV resources will be available at the `/` URI. If defined it must be an absolute URI, for example `/dav`. Default: "".
-    - `proxy_allowed`, list of IP addresses and IP ranges allowed to set `X-Forwarded-For`, `X-Real-IP`, `CF-Connecting-IP`, `True-Client-IP` headers. Any of the indicated headers, if set on requests from a connection address not in this list, will be silently ignored. Default: empty.
+    - `proxy_allowed`, list of IP addresses and IP ranges allowed to set client IP proxy header such as `X-Forwarded-For`. Any client IP proxy headers, if set on requests from a connection address not in this list, will be silently ignored. Default: empty.
+    - `client_ip_proxy_header`, string. Defines the allowed client IP proxy header such as `X-Forwarded-For`, `X-Real-IP` etc. Default: empty
+    - `client_ip_header_depth`, integer. Some client IP headers such as `X-Forwarded-For` can contain multiple IP address, this setting define the position to trust starting from the right. For example if we have: `10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1` and the depth is `0`, SFTPGo will use `13.0.0.1` as client IP, if depth is `1`, `12.0.0.1` will be used and so on. Default: `0`.
   - `certificate_file`, string. Certificate for WebDAV over HTTPS. This can be an absolute path or a path relative to the config dir.
   - `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. A certificate and a private key are required to enable HTTPS connections. Certificate and key files can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows.
   - `ca_certificates`, list of strings. Set of root certificate authorities to be used to verify client certificates.
@@ -262,8 +264,10 @@ The configuration file contains the following sections:
     - `certificate_key_file`, string. Binding specific private key matching the above certificate. This can be an absolute path or a path relative to the config dir. If not set the global ones will be used, if any.
     - `min_tls_version`, integer. Defines the minimum version of TLS to be enabled. `12` means TLS 1.2 (and therefore TLS 1.2 and TLS 1.3 will be enabled),`13` means TLS 1.3. Default: `12`.
     - `client_auth_type`, integer. Set to `1` to require client certificate authentication in addition to JWT/Web authentication. You need to define at least a certificate authority for this to work. Default: 0.
-    - `tls_cipher_suites`, list of strings. List of supported cipher suites for TLS version 1.2. If empty, a default list of secure cipher suites is used, with a preference order based on hardware performance. Note that TLS 1.3 ciphersuites are not configurable. The supported ciphersuites names are defined [here](https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L52). Any invalid name will be silently ignored. The order matters, the ciphers listed first will be the preferred ones. Default: blank.
-    - `proxy_allowed`, list of IP addresses and IP ranges allowed to set `X-Forwarded-For`, `X-Real-IP`, `X-Forwarded-Proto`, `CF-Connecting-IP`, `True-Client-IP` and any other headers defined in the `security` section. Any of the indicated headers, if set on requests from a connection address not in this list, will be silently ignored. Default: blank.
+    - `tls_cipher_suites`, list of strings. List of supported cipher suites for TLS version 1.2. If empty, a default list of secure cipher suites is used, with a preference order based on hardware performance. Note that TLS 1.3 ciphersuites are not configurable. The supported ciphersuites names are defined [here](https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L52). Any invalid name will be silently ignored. The order matters, the ciphers listed first will be the preferred ones. Default: empty.
+    - `proxy_allowed`, list of IP addresses and IP ranges allowed to set client IP proxy header such as `X-Forwarded-For`, `X-Real-IP` and any other headers defined in the `security` section. Any of the indicated headers, if set on requests from a connection address not in this list, will be silently ignored. Default: empty.
+    - `client_ip_proxy_header`, string. Defines the allowed client IP proxy header such as `X-Forwarded-For`, `X-Real-IP` etc. Default: empty
+    - `client_ip_header_depth`, integer. Some client IP headers such as `X-Forwarded-For` can contain multiple IP address, this setting define the position to trust starting from the right. For example if we have: `10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1` and the depth is `0`, SFTPGo will use `13.0.0.1` as client IP, if depth is `1`, `12.0.0.1` will be used and so on. Default: `0`.
     - `hide_login_url`, integer. If both web admin and web client are enabled each login page will show a link to the other one. This setting allows to hide this link. 0 means that the login links are displayed on both admin and client login page. This is the default. 1 means that the login link to the web client login page is hidden on admin login page. 2 means that the login link to the web admin login page is hidden on client login page. The flags can be combined, for example 3 will disable both login links.
     - `render_openapi`, boolean. Set to `false` to disable serving of the OpenAPI schema and renderer. Default `true`.
     - `web_client_integrations`, list of struct. The SFTPGo web client allows to send the files with the specified extensions to the configured URL using the [postMessage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage). This way you can integrate your own file viewer or editor. Take a look at the commentented example [here](../examples/webclient-integrations/test.html) to understand how to use this feature. Each struct has the following fields:

+ 9 - 2
httpd/httpd.go

@@ -417,9 +417,16 @@ type Binding struct {
 	// any invalid name will be silently ignored.
 	// The order matters, the ciphers listed first will be the preferred ones.
 	TLSCipherSuites []string `json:"tls_cipher_suites" mapstructure:"tls_cipher_suites"`
-	// List of IP addresses and IP ranges allowed to set X-Forwarded-For, X-Real-IP,
-	// X-Forwarded-Proto headers.
+	// List of IP addresses and IP ranges allowed to set client IP proxy headers and
+	// X-Forwarded-Proto header.
 	ProxyAllowed []string `json:"proxy_allowed" mapstructure:"proxy_allowed"`
+	// Allowed client IP proxy header such as "X-Forwarded-For", "X-Real-IP"
+	ClientIPProxyHeader string `json:"client_ip_proxy_header" mapstructure:"client_ip_proxy_header"`
+	// Some client IP headers such as "X-Forwarded-For" can contain multiple IP address, this setting
+	// define the position to trust starting from the right. For example if we have:
+	// "10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1" and the depth is 0, SFTPGo will use "13.0.0.1"
+	// as client IP, if depth is 1, "12.0.0.1" will be used and so on
+	ClientIPHeaderDepth int `json:"client_ip_header_depth" mapstructure:"client_ip_header_depth"`
 	// If both web admin and web client are enabled each login page will show a link
 	// to the other one. This setting allows to hide this link:
 	// - 0 login links are displayed on both admin and client login page. This is the default

+ 6 - 5
httpd/internal_test.go

@@ -1551,11 +1551,12 @@ func TestProxyHeaders(t *testing.T) {
 	testIP := "10.29.1.9"
 	validForwardedFor := "172.19.2.6"
 	b := Binding{
-		Address:         "",
-		Port:            8080,
-		EnableWebAdmin:  true,
-		EnableWebClient: false,
-		ProxyAllowed:    []string{testIP, "10.8.0.0/30"},
+		Address:             "",
+		Port:                8080,
+		EnableWebAdmin:      true,
+		EnableWebClient:     false,
+		ProxyAllowed:        []string{testIP, "10.8.0.0/30"},
+		ClientIPProxyHeader: "x-forwarded-for",
 	}
 	err = b.parseAllowedProxy()
 	assert.NoError(t, err)

+ 1 - 1
httpd/server.go

@@ -977,7 +977,7 @@ func (s *httpdServer) checkConnection(next http.Handler) http.Handler {
 		if ip != nil {
 			for _, allow := range s.binding.allowHeadersFrom {
 				if allow(ip) {
-					parsedIP := util.GetRealIP(r)
+					parsedIP := util.GetRealIP(r, s.binding.ClientIPProxyHeader, s.binding.ClientIPHeaderDepth)
 					if parsedIP != "" {
 						ipAddr = parsedIP
 						r.RemoteAddr = ipAddr

+ 5 - 1
sftpgo.json

@@ -149,7 +149,9 @@
         "client_auth_type": 0,
         "tls_cipher_suites": [],
         "prefix": "",
-        "proxy_allowed": []
+        "proxy_allowed": [],
+        "client_ip_proxy_header": "",
+        "client_ip_header_depth": 0
       }
     ],
     "certificate_file": "",
@@ -251,6 +253,8 @@
         "client_auth_type": 0,
         "tls_cipher_suites": [],
         "proxy_allowed": [],
+        "client_ip_proxy_header": "",
+        "client_ip_header_depth": 0,
         "hide_login_url": 0,
         "render_openapi": true,
         "web_client_integrations": [],

+ 20 - 22
util/util.go

@@ -41,11 +41,7 @@ const (
 )
 
 var (
-	xForwardedFor  = http.CanonicalHeaderKey("X-Forwarded-For")
-	xRealIP        = http.CanonicalHeaderKey("X-Real-IP")
-	cfConnectingIP = http.CanonicalHeaderKey("CF-Connecting-IP")
-	trueClientIP   = http.CanonicalHeaderKey("True-Client-IP")
-	emailRegex     = regexp.MustCompile("^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$")
+	emailRegex = regexp.MustCompile("^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$")
 )
 
 // Contains reports whether v is present in elems.
@@ -536,27 +532,29 @@ func GetSSHPublicKeyAsString(pubKey []byte) (string, error) {
 
 // GetRealIP returns the ip address as result of parsing either the
 // X-Real-IP header or the X-Forwarded-For header
-func GetRealIP(r *http.Request) string {
-	var ip string
-
-	if clientIP := r.Header.Get(trueClientIP); clientIP != "" {
-		ip = clientIP
-	} else if xrip := r.Header.Get(xRealIP); xrip != "" {
-		ip = xrip
-	} else if clientIP := r.Header.Get(cfConnectingIP); clientIP != "" {
-		ip = clientIP
-	} else if xff := r.Header.Get(xForwardedFor); xff != "" {
-		i := strings.Index(xff, ",")
-		if i == -1 {
-			i = len(xff)
+func GetRealIP(r *http.Request, header string, depth int) string {
+	if header == "" {
+		return ""
+	}
+	var ipAddresses []string
+
+	for _, h := range r.Header.Values(header) {
+		for _, ipStr := range strings.Split(h, ",") {
+			ipStr = strings.TrimSpace(ipStr)
+			ipAddresses = append(ipAddresses, ipStr)
 		}
-		ip = strings.TrimSpace(xff[:i])
 	}
-	if ip == "" || net.ParseIP(ip) == nil {
-		return ""
+
+	idx := len(ipAddresses) - 1 - depth
+	if idx >= 0 {
+		ip := strings.TrimSpace(ipAddresses[idx])
+		if ip == "" || net.ParseIP(ip) == nil {
+			return ""
+		}
+		return ip
 	}
 
-	return ip
+	return ""
 }
 
 // GetHTTPLocalAddress returns the local address for an http.Request

+ 33 - 17
webdavd/internal_test.go

@@ -435,19 +435,26 @@ func TestRemoteAddress(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Empty(t, req.RemoteAddr)
 
-	req.Header.Set("True-Client-IP", remoteAddr1)
-	ip := util.GetRealIP(req)
+	trueClientIP := "True-Client-IP"
+	cfConnectingIP := "CF-Connecting-IP"
+	xff := "X-Forwarded-For"
+	xRealIP := "X-Real-IP"
+
+	req.Header.Set(trueClientIP, remoteAddr1)
+	ip := util.GetRealIP(req, trueClientIP, 0)
 	assert.Equal(t, remoteAddr1, ip)
-	req.Header.Del("True-Client-IP")
-	req.Header.Set("CF-Connecting-IP", remoteAddr1)
-	ip = util.GetRealIP(req)
+	ip = util.GetRealIP(req, trueClientIP, 2)
+	assert.Empty(t, ip)
+	req.Header.Del(trueClientIP)
+	req.Header.Set(cfConnectingIP, remoteAddr1)
+	ip = util.GetRealIP(req, cfConnectingIP, 0)
 	assert.Equal(t, remoteAddr1, ip)
-	req.Header.Del("CF-Connecting-IP")
-	req.Header.Set("X-Forwarded-For", remoteAddr1)
-	ip = util.GetRealIP(req)
+	req.Header.Del(cfConnectingIP)
+	req.Header.Set(xff, remoteAddr1)
+	ip = util.GetRealIP(req, xff, 0)
 	assert.Equal(t, remoteAddr1, ip)
 	// this will be ignored, remoteAddr1 is not allowed to se this header
-	req.Header.Set("X-Forwarded-For", remoteAddr2)
+	req.Header.Set(xff, remoteAddr2)
 	req.RemoteAddr = remoteAddr1
 	ip = server.checkRemoteAddress(req)
 	assert.Equal(t, remoteAddr1, ip)
@@ -455,32 +462,41 @@ func TestRemoteAddress(t *testing.T) {
 	ip = server.checkRemoteAddress(req)
 	assert.Empty(t, ip)
 
-	req.Header.Set("X-Forwarded-For", fmt.Sprintf("%v, %v", remoteAddr2, remoteAddr1))
-	ip = util.GetRealIP(req)
+	req.Header.Set(xff, fmt.Sprintf("%v , %v", remoteAddr2, remoteAddr1))
+	ip = util.GetRealIP(req, xff, 1)
 	assert.Equal(t, remoteAddr2, ip)
 
 	req.RemoteAddr = remoteAddr2
-	req.Header.Set("X-Forwarded-For", fmt.Sprintf("%v,%v", "12.34.56.78", "172.16.2.4"))
+	req.Header.Set(xff, fmt.Sprintf("%v,%v", "12.34.56.78", "172.16.2.4"))
+	server.binding.ClientIPHeaderDepth = 1
+	server.binding.ClientIPProxyHeader = xff
 	ip = server.checkRemoteAddress(req)
 	assert.Equal(t, "12.34.56.78", ip)
 	assert.Equal(t, ip, req.RemoteAddr)
 
+	req.RemoteAddr = remoteAddr2
+	req.Header.Set(xff, fmt.Sprintf("%v,%v", "12.34.56.79", "172.16.2.5"))
+	server.binding.ClientIPHeaderDepth = 0
+	ip = server.checkRemoteAddress(req)
+	assert.Equal(t, "172.16.2.5", ip)
+	assert.Equal(t, ip, req.RemoteAddr)
+
 	req.RemoteAddr = "10.8.0.2"
-	req.Header.Set("X-Forwarded-For", remoteAddr1)
+	req.Header.Set(xff, remoteAddr1)
 	ip = server.checkRemoteAddress(req)
 	assert.Equal(t, remoteAddr1, ip)
 	assert.Equal(t, ip, req.RemoteAddr)
 
 	req.RemoteAddr = "10.8.0.3"
-	req.Header.Set("X-Forwarded-For", "not an ip")
+	req.Header.Set(xff, "not an ip")
 	ip = server.checkRemoteAddress(req)
 	assert.Equal(t, "10.8.0.3", ip)
 	assert.Equal(t, ip, req.RemoteAddr)
 
-	req.Header.Del("X-Forwarded-For")
+	req.Header.Del(xff)
 	req.RemoteAddr = ""
-	req.Header.Set("X-Real-IP", remoteAddr1)
-	ip = util.GetRealIP(req)
+	req.Header.Set(xRealIP, remoteAddr1)
+	ip = util.GetRealIP(req, "x-real-ip", 0)
 	assert.Equal(t, remoteAddr1, ip)
 	req.RemoteAddr = ""
 }

+ 1 - 1
webdavd/server.go

@@ -335,7 +335,7 @@ func (s *webDavServer) checkRemoteAddress(r *http.Request) string {
 	if ip != nil {
 		for _, allow := range s.binding.allowHeadersFrom {
 			if allow(ip) {
-				parsedIP := util.GetRealIP(r)
+				parsedIP := util.GetRealIP(r, s.binding.ClientIPProxyHeader, s.binding.ClientIPHeaderDepth)
 				if parsedIP != "" {
 					ipAddr = parsedIP
 					r.RemoteAddr = ipAddr

+ 10 - 3
webdavd/webdavd.go

@@ -96,9 +96,16 @@ type Binding struct {
 	// Prefix for WebDAV resources, if empty WebDAV resources will be available at the
 	// root ("/") URI. If defined it must be an absolute URI.
 	Prefix string `json:"prefix" mapstructure:"prefix"`
-	// List of IP addresses and IP ranges allowed to set X-Forwarded-For/X-Real-IP headers.
-	ProxyAllowed     []string `json:"proxy_allowed" mapstructure:"proxy_allowed"`
-	allowHeadersFrom []func(net.IP) bool
+	// List of IP addresses and IP ranges allowed to set client IP proxy headers
+	ProxyAllowed []string `json:"proxy_allowed" mapstructure:"proxy_allowed"`
+	// Allowed client IP proxy header such as "X-Forwarded-For", "X-Real-IP"
+	ClientIPProxyHeader string `json:"client_ip_proxy_header" mapstructure:"client_ip_proxy_header"`
+	// Some client IP headers such as "X-Forwarded-For" can contain multiple IP address, this setting
+	// define the position to trust starting from the right. For example if we have:
+	// "10.0.0.1,11.0.0.1,12.0.0.1,13.0.0.1" and the depth is 0, SFTPGo will use "13.0.0.1"
+	// as client IP, if depth is 1, "12.0.0.1" will be used and so on
+	ClientIPHeaderDepth int `json:"client_ip_header_depth" mapstructure:"client_ip_header_depth"`
+	allowHeadersFrom    []func(net.IP) bool
 }
 
 func (b *Binding) parseAllowedProxy() error {