Ver Fonte

httpd: allow to restrict allowed hosts ...

... and to add security headers to the responses

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Nicola Murino há 3 anos atrás
pai
commit
f1a255aa6c
15 ficheiros alterados com 415 adições e 24 exclusões
  1. 121 0
      config/config.go
  2. 48 0
      config/config_test.go
  3. 16 2
      docs/full-configuration.md
  4. 4 4
      ftpd/handler.go
  5. 5 4
      go.mod
  6. 10 7
      go.sum
  7. 1 1
      httpd/handler.go
  8. 65 1
      httpd/httpd.go
  9. 9 0
      httpd/httpd_test.go
  10. 78 0
      httpd/internal_test.go
  11. 37 0
      httpd/server.go
  12. 3 2
      sftpd/handler.go
  13. 1 1
      sftpd/scp.go
  14. 15 0
      sftpgo.json
  15. 2 2
      webdavd/handler.go

+ 121 - 0
config/config.go

@@ -92,6 +92,21 @@ var (
 			UsernameField:   "",
 			RoleField:       "",
 		},
+		Security: httpd.SecurityConf{
+			Enabled:                 false,
+			AllowedHosts:            nil,
+			AllowedHostsAreRegex:    false,
+			HostsProxyHeaders:       nil,
+			HTTPSProxyHeaders:       nil,
+			STSSeconds:              0,
+			STSIncludeSubdomains:    false,
+			STSPreload:              false,
+			ContentTypeNosniff:      false,
+			ContentSecurityPolicy:   "",
+			PermissionsPolicy:       "",
+			CrossOriginOpenerPolicy: "",
+			ExpectCTHeader:          "",
+		},
 	}
 	defaultRateLimiter = common.RateLimiterConfig{
 		Average:                0,
@@ -1083,6 +1098,106 @@ func getWebDAVDBindingFromEnv(idx int) {
 	}
 }
 
+func getHTTPDSecurityProxyHeadersFromEnv(idx int) []httpd.HTTPSProxyHeader {
+	var httpsProxyHeaders []httpd.HTTPSProxyHeader
+
+	for subIdx := 0; subIdx < 10; subIdx++ {
+		proxyKey, _ := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__SECURITY__HTTPS_PROXY_HEADERS__%v__KEY", idx, subIdx))
+		proxyVal, _ := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__SECURITY__HTTPS_PROXY_HEADERS__%v__VALUE", idx, subIdx))
+		if proxyKey != "" && proxyVal != "" {
+			httpsProxyHeaders = append(httpsProxyHeaders, httpd.HTTPSProxyHeader{
+				Key:   proxyKey,
+				Value: proxyVal,
+			})
+		}
+	}
+	return httpsProxyHeaders
+}
+
+func getHTTPDSecurityConfFromEnv(idx int) (httpd.SecurityConf, bool) {
+	var result httpd.SecurityConf
+	isSet := false
+
+	enabled, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__SECURITY__ENABLED", idx))
+	if ok {
+		result.Enabled = enabled
+		isSet = true
+	}
+
+	allowedHosts, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__SECURITY__ALLOWED_HOSTS", idx))
+	if ok {
+		result.AllowedHosts = allowedHosts
+		isSet = true
+	}
+
+	allowedHostsAreRegex, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__SECURITY__ALLOWED_HOSTS_ARE_REGEX", idx))
+	if ok {
+		result.AllowedHostsAreRegex = allowedHostsAreRegex
+		isSet = true
+	}
+
+	hostsProxyHeaders, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__SECURITY__HOSTS_PROXY_HEADERS", idx))
+	if ok {
+		result.HostsProxyHeaders = hostsProxyHeaders
+		isSet = true
+	}
+
+	httpsProxyHeaders := getHTTPDSecurityProxyHeadersFromEnv(idx)
+	if len(httpsProxyHeaders) > 0 {
+		result.HTTPSProxyHeaders = httpsProxyHeaders
+		isSet = true
+	}
+
+	stsSeconds, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__SECURITY__STS_SECONDS", idx))
+	if ok {
+		result.STSSeconds = stsSeconds
+	}
+
+	stsIncludeSubDomains, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__SECURITY__STS_INCLUDE_SUBDOMAINS", idx))
+	if ok {
+		result.STSIncludeSubdomains = stsIncludeSubDomains
+		isSet = true
+	}
+
+	stsPreload, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__SECURITY__STS_PRELOAD", idx))
+	if ok {
+		result.STSPreload = stsPreload
+		isSet = true
+	}
+
+	contentTypeNosniff, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__SECURITY__CONTENT_TYPE_NOSNIFF", idx))
+	if ok {
+		result.ContentTypeNosniff = contentTypeNosniff
+		isSet = true
+	}
+
+	contentSecurityPolicy, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__SECURITY__CONTENT_SECURITY_POLICY", idx))
+	if ok {
+		result.ContentSecurityPolicy = contentSecurityPolicy
+		isSet = true
+	}
+
+	permissionsPolicy, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__SECURITY__PERMISSIONS_POLICY", idx))
+	if ok {
+		result.PermissionsPolicy = permissionsPolicy
+		isSet = true
+	}
+
+	crossOriginOpenedPolicy, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__SECURITY__CROSS_ORIGIN_OPENER_POLICY", idx))
+	if ok {
+		result.CrossOriginOpenerPolicy = crossOriginOpenedPolicy
+		isSet = true
+	}
+
+	expectCTHeader, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__SECURITY__EXPECT_CT_HEADER", idx))
+	if ok {
+		result.ExpectCTHeader = expectCTHeader
+		isSet = true
+	}
+
+	return result, isSet
+}
+
 func getHTTPDOIDCFromEnv(idx int) (httpd.OIDC, bool) {
 	var result httpd.OIDC
 	isSet := false
@@ -1246,6 +1361,12 @@ func getHTTPDBindingFromEnv(idx int) {
 		isSet = true
 	}
 
+	securityConf, ok := getHTTPDSecurityConfFromEnv(idx)
+	if ok {
+		binding.Security = securityConf
+		isSet = true
+	}
+
 	setHTTPDBinding(isSet, binding, idx)
 }
 

+ 48 - 0
config/config_test.go

@@ -819,6 +819,20 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__OIDC__REDIRECT_BASE_URL", "redirect base url")
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__OIDC__USERNAME_FIELD", "preferred_username")
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__OIDC__ROLE_FIELD", "sftpgo_role")
+	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__ENABLED", "true")
+	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__ALLOWED_HOSTS", "*.example.com,*.example.net")
+	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__ALLOWED_HOSTS_ARE_REGEX", "1")
+	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__HOSTS_PROXY_HEADERS", "X-Forwarded-Host")
+	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__HTTPS_PROXY_HEADERS__1__KEY", "X-Forwarded-Proto")
+	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__HTTPS_PROXY_HEADERS__1__VALUE", "https")
+	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__STS_SECONDS", "31536000")
+	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__STS_INCLUDE_SUBDOMAINS", "false")
+	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__STS_PRELOAD", "0")
+	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CONTENT_TYPE_NOSNIFF", "t")
+	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CONTENT_SECURITY_POLICY", "script-src $NONCE")
+	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__PERMISSIONS_POLICY", "fullscreen=(), geolocation=()")
+	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CROSS_ORIGIN_OPENER_POLICY", "same-origin")
+	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__EXPECT_CT_HEADER", `max-age=86400, enforce, report-uri="https://foo.example/report"`)
 	t.Cleanup(func() {
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__0__ADDRESS")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__0__PORT")
@@ -848,6 +862,20 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__OIDC__REDIRECT_BASE_URL")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__OIDC__USERNAME_FIELD")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__OIDC__ROLE_FIELD")
+		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__ENABLED")
+		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__ALLOWED_HOSTS")
+		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__ALLOWED_HOSTS_ARE_REGEX")
+		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__HOSTS_PROXY_HEADERS")
+		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__HTTPS_PROXY_HEADERS__1__KEY")
+		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__HTTPS_PROXY_HEADERS__1__VALUE")
+		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__STS_SECONDS")
+		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__STS_INCLUDE_SUBDOMAINS")
+		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__STS_PRELOAD")
+		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CONTENT_TYPE_NOSNIFF")
+		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CONTENT_SECURITY_POLICY")
+		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__PERMISSIONS_POLICY")
+		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CROSS_ORIGIN_OPENER_POLICY")
+		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__EXPECT_CT_HEADER")
 	})
 
 	configDir := ".."
@@ -866,6 +894,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 	require.Empty(t, bindings[0].OIDC.ConfigURL)
 	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, 8000, bindings[1].Port)
 	require.Equal(t, "127.0.0.1", bindings[1].Address)
 	require.False(t, bindings[1].EnableHTTPS)
@@ -876,6 +905,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 	require.Nil(t, bindings[1].TLSCipherSuites)
 	require.Equal(t, 1, bindings[1].HideLoginURL)
 	require.Empty(t, bindings[1].OIDC.ClientID)
+	require.False(t, bindings[1].Security.Enabled)
 	require.Equal(t, 9000, bindings[2].Port)
 	require.Equal(t, "127.0.1.1", bindings[2].Address)
 	require.True(t, bindings[2].EnableHTTPS)
@@ -900,6 +930,24 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 	require.Equal(t, "redirect base url", bindings[2].OIDC.RedirectBaseURL)
 	require.Equal(t, "preferred_username", bindings[2].OIDC.UsernameField)
 	require.Equal(t, "sftpgo_role", bindings[2].OIDC.RoleField)
+	require.True(t, bindings[2].Security.Enabled)
+	require.Len(t, bindings[2].Security.AllowedHosts, 2)
+	require.Equal(t, "*.example.com", bindings[2].Security.AllowedHosts[0])
+	require.Equal(t, "*.example.net", bindings[2].Security.AllowedHosts[1])
+	require.True(t, bindings[2].Security.AllowedHostsAreRegex)
+	require.Len(t, bindings[2].Security.HostsProxyHeaders, 1)
+	require.Equal(t, "X-Forwarded-Host", bindings[2].Security.HostsProxyHeaders[0])
+	require.Len(t, bindings[2].Security.HTTPSProxyHeaders, 1)
+	require.Equal(t, "X-Forwarded-Proto", bindings[2].Security.HTTPSProxyHeaders[0].Key)
+	require.Equal(t, "https", bindings[2].Security.HTTPSProxyHeaders[0].Value)
+	require.Equal(t, int64(31536000), bindings[2].Security.STSSeconds)
+	require.False(t, bindings[2].Security.STSIncludeSubdomains)
+	require.False(t, bindings[2].Security.STSPreload)
+	require.True(t, bindings[2].Security.ContentTypeNosniff)
+	require.Equal(t, "script-src $NONCE", bindings[2].Security.ContentSecurityPolicy)
+	require.Equal(t, "fullscreen=(), geolocation=()", bindings[2].Security.PermissionsPolicy)
+	require.Equal(t, "same-origin", bindings[2].Security.CrossOriginOpenerPolicy)
+	require.Equal(t, `max-age=86400, enforce, report-uri="https://foo.example/report"`, bindings[2].Security.ExpectCTHeader)
 }
 
 func TestHTTPClientCertificatesFromEnv(t *testing.T) {

+ 16 - 2
docs/full-configuration.md

@@ -230,7 +230,7 @@ The configuration file contains the following sections:
     - `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` headers. Any of the indicated headers, if set on requests from a connection address not in this list, will be silently ignored. 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.
     - `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:
@@ -243,6 +243,20 @@ The configuration file contains the following sections:
       - `redirect_base_url`, string. Defines the base URL to redirect to after OpenID authentication. The suffix `/web/oidc/redirect` will be added to this base URL, adding also the `web_root` if configured. Default: blank.
       - `username_field`, string. Defines the ID token claims field to map to the SFTPGo username. Default: blank.
       - `role_field`, string. Defines the optional ID token claims field to map to a SFTPGo role. If the defined ID token claims field is set to `admin` the authenticated user is mapped to an SFTPGo admin. You don't need to specify this field if you want to use OpenID only for the Web Client UI. Default: blank.
+    - `security`, struct. Defines security headers to add to HTTP responses and allows to restrict allowed hosts. The following parameters are supported:
+      - `enabled`, boolean. Set to `true` to enable security configurations. Default: `false`.
+      - `allowed_hosts`, list of strings. Fully qualified domain names that are allowed. An empty list allows any and all host names. Default: empty.
+      - `allowed_hosts_are_regex`, boolean. Determines if the provided allowed hosts contains valid regular expressions. Default: `false`.
+      - `hosts_proxy_headers`, list of string. Defines a set of header keys that may hold a proxied hostname value for the request, for example `X-Forwarded-Host`. Default: empty.
+      - `https_proxy_headers`, list of struct, each struct contains the fields `key` and `value`. Defines a a list of header keys with associated values that would indicate a valid https request. For example `key` could be `X-Forwarded-Proto` and `value` `https`. Default: empty.
+      - `sts_seconds`, integer. Defines the max-age of the `Strict-Transport-Security` header. This header will be included for `https` responses or for HTTP request if the request includes a defined HTTPS proxy header. Default: `0`, which would NOT include the header.
+      - `sts_include_subdomains`, boolean. Set to `true`, the `includeSubdomains` will be appended to the `Strict-Transport-Security` header. Default: `false`.
+      - `sts_preload`, boolean. Set to true, the `preload` flag will be appended to the `Strict-Transport-Security` header. Default: `false`.
+      - `content_type_nosniff`, boolean. Set to `true` to add the `X-Content-Type-Options` header with the value `nosniff` Default: `false`.
+      - `content_security_policy`, string. Allows to set the `Content-Security-Policy` header value. Default: blank.
+      - `permissions_policy`, string. Allows to set the `Permissions-Policy` header value. Default: blank.
+      - `cross_origin_opener_policy`, string. Allows to set the `Cross-Origin-Opener-Policy` header value. Default: blank.
+      - `expect_ct_header`, string. Allows to set the `Expect-CT` header value. Default: blank.
   - `templates_path`, string. Path to the HTML web templates. This can be an absolute path or a path relative to the config dir
   - `static_files_path`, string. Path to the static files for the web interface. This can be an absolute path or a path relative to the config dir. If both `templates_path` and `static_files_path` are empty the built-in web interface will be disabled
   - `backups_path`, string. Path to the backup directory. This can be an absolute path or a path relative to the config dir. We don't allow backups in arbitrary paths for security reasons
@@ -255,7 +269,7 @@ The configuration file contains the following sections:
   - `signing_passphrase`, string. Passphrase to use to derive the signing key for JWT and CSRF tokens. If empty a random signing key will be generated each time SFTPGo starts. If you set a signing passphrase you should consider rotating it periodically for added security.
   - `max_upload_file_size`, integer. Defines the maximum request body size, in bytes, for Web Client/API HTTP upload requests. 0 means no limit. Default: 1048576000.
   - `cors` struct containing CORS configuration. SFTPGo uses [Go CORS handler](https://github.com/rs/cors), please refer to upstream documentation for fields meaning and their default values.
-    - `enabled`, boolean, set to true to enable CORS.
+    - `enabled`, boolean, set to `true` to enable CORS.
     - `allowed_origins`, list of strings.
     - `allowed_methods`, list of strings.
     - `allowed_headers`, list of strings.

+ 4 - 4
ftpd/handler.go

@@ -354,7 +354,7 @@ func (c *Connection) uploadFile(fs vfs.Fs, fsPath, ftpPath string, flags int) (f
 		if !c.User.HasPerm(dataprovider.PermUpload, path.Dir(ftpPath)) {
 			return nil, fmt.Errorf("%w, no upload permission", ftpserver.ErrFileNameNotAllowed)
 		}
-		return c.handleFTPUploadToNewFile(fs, fsPath, filePath, ftpPath)
+		return c.handleFTPUploadToNewFile(fs, flags, fsPath, filePath, ftpPath)
 	}
 
 	if statErr != nil {
@@ -375,7 +375,7 @@ func (c *Connection) uploadFile(fs vfs.Fs, fsPath, ftpPath string, flags int) (f
 	return c.handleFTPUploadToExistingFile(fs, flags, fsPath, filePath, stat.Size(), ftpPath)
 }
 
-func (c *Connection) handleFTPUploadToNewFile(fs vfs.Fs, resolvedPath, filePath, requestPath string) (ftpserver.FileTransfer, error) {
+func (c *Connection) handleFTPUploadToNewFile(fs vfs.Fs, flags int, resolvedPath, filePath, requestPath string) (ftpserver.FileTransfer, error) {
 	diskQuota, transferQuota := c.HasSpace(true, false, requestPath)
 	if !diskQuota.HasSpace || !transferQuota.HasUploadSpace() {
 		c.Log(logger.LevelInfo, "denying file write due to quota limits")
@@ -385,9 +385,9 @@ func (c *Connection) handleFTPUploadToNewFile(fs vfs.Fs, resolvedPath, filePath,
 		c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
 		return nil, fmt.Errorf("%w, denied by pre-upload action", ftpserver.ErrFileNameNotAllowed)
 	}
-	file, w, cancelFn, err := fs.Create(filePath, 0)
+	file, w, cancelFn, err := fs.Create(filePath, flags)
 	if err != nil {
-		c.Log(logger.LevelError, "error creating file %#v: %+v", resolvedPath, err)
+		c.Log(logger.LevelError, "error creating file %#v, flags %v: %+v", resolvedPath, flags, err)
 		return nil, c.GetFsError(fs, err)
 	}
 

+ 5 - 4
go.mod

@@ -7,7 +7,7 @@ require (
 	github.com/Azure/azure-storage-blob-go v0.14.0
 	github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
 	github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
-	github.com/aws/aws-sdk-go v1.42.53
+	github.com/aws/aws-sdk-go v1.43.0
 	github.com/cockroachdb/cockroach-go/v2 v2.2.8
 	github.com/coreos/go-oidc/v3 v3.1.0
 	github.com/eikenb/pipeat v0.0.0-20210603033007-44fc3ffce52b
@@ -25,7 +25,7 @@ require (
 	github.com/hashicorp/go-plugin v1.4.3
 	github.com/hashicorp/go-retryablehttp v0.7.0
 	github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
-	github.com/klauspost/compress v1.14.2
+	github.com/klauspost/compress v1.14.3
 	github.com/lestrrat-go/jwx v1.2.18
 	github.com/lib/pq v1.10.4
 	github.com/lithammer/shortuuid/v3 v3.0.7
@@ -47,6 +47,7 @@ require (
 	github.com/spf13/viper v1.10.1
 	github.com/stretchr/testify v1.7.0
 	github.com/studio-b12/gowebdav v0.0.0-20220128162035-c7b1ff8a5e62
+	github.com/unrolled/secure v1.10.0
 	github.com/wagslane/go-password-validator v0.3.0
 	github.com/xhit/go-simple-mail/v2 v2.10.0
 	github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a
@@ -109,7 +110,7 @@ require (
 	github.com/pelletier/go-toml v1.9.4 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
-	github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
+	github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect
 	github.com/prometheus/client_model v0.2.0 // indirect
 	github.com/prometheus/common v0.32.1 // indirect
 	github.com/prometheus/procfs v0.7.3 // indirect
@@ -127,7 +128,7 @@ require (
 	golang.org/x/tools v0.1.9 // indirect
 	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
-	google.golang.org/genproto v0.0.0-20220215190005-e57b466719ef // indirect
+	google.golang.org/genproto v0.0.0-20220217155828-d576998c0009 // indirect
 	google.golang.org/grpc v1.44.0 // indirect
 	google.golang.org/protobuf v1.27.1 // indirect
 	gopkg.in/ini.v1 v1.66.4 // indirect

+ 10 - 7
go.sum

@@ -143,8 +143,8 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI
 github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
 github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
 github.com/aws/aws-sdk-go v1.40.34/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
-github.com/aws/aws-sdk-go v1.42.53 h1:56T04NWcmc0ZVYFbUc6HdewDQ9iHQFlmS6hj96dRjJs=
-github.com/aws/aws-sdk-go v1.42.53/go.mod h1:OGr6lGMAKGlG9CVrYnWYDKIyb829c6EVBRjxqjmPepc=
+github.com/aws/aws-sdk-go v1.43.0 h1:y4UrPbxU/mIL08qksVPE/nwH9IXuC1udjOaNyhEe+pI=
+github.com/aws/aws-sdk-go v1.43.0/go.mod h1:OGr6lGMAKGlG9CVrYnWYDKIyb829c6EVBRjxqjmPepc=
 github.com/aws/aws-sdk-go-v2 v1.9.0/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
 github.com/aws/aws-sdk-go-v2/config v1.7.0/go.mod h1:w9+nMZ7soXCe5nT46Ri354SNhXDQ6v+V5wqDjnZE+GY=
 github.com/aws/aws-sdk-go-v2/credentials v1.4.0/go.mod h1:dgGR+Qq7Wjcd4AOAW5Rf5Tnv3+x7ed6kETXyS9WCuAY=
@@ -511,8 +511,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
 github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
-github.com/klauspost/compress v1.14.2 h1:S0OHlFk/Gbon/yauFJ4FfJJF5V0fc5HbBTJazi28pRw=
-github.com/klauspost/compress v1.14.2/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
+github.com/klauspost/compress v1.14.3 h1:DQv1WP+iS4srNjibdnHtqu8JNWCDMluj5NzPnFJsnvk=
+github.com/klauspost/compress v1.14.3/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
 github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 github.com/klauspost/cpuid/v2 v2.0.11 h1:i2lw1Pm7Yi/4O6XCSyJWqEHI2MDw2FzUK6o/D21xn2A=
 github.com/klauspost/cpuid/v2 v2.0.11/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
@@ -645,8 +645,9 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
 github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
-github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
 github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
+github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c h1:NRoLoZvkBTKvR5gQLgA3e0hqjkY9u1wm+iOL45VN/qI=
+github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
 github.com/pquerna/otp v1.3.0 h1:oJV/SkzR33anKXwQU3Of42rL4wbrffP4uvUf1SvS5Xs=
 github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
@@ -743,6 +744,8 @@ github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hM
 github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
 github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
 github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
+github.com/unrolled/secure v1.10.0 h1:TBNP42z2AB+2pW9PR6vdbqhlQuv1iTeSVzK1qHjOBzA=
+github.com/unrolled/secure v1.10.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
 github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I=
 github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ=
 github.com/xhit/go-simple-mail/v2 v2.10.0 h1:nib6RaJ4qVh5HD9UE9QJqnUZyWp3upv+Z6CFxaMj0V8=
@@ -1174,8 +1177,8 @@ google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ6
 google.golang.org/genproto v0.0.0-20220201184016-50beb8ab5c44/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
 google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
 google.golang.org/genproto v0.0.0-20220211171837-173942840c17/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
-google.golang.org/genproto v0.0.0-20220215190005-e57b466719ef h1:LgGaJzny+/at3jTXZUNh/l8VBWyAiskCHrwq6iEYE7I=
-google.golang.org/genproto v0.0.0-20220215190005-e57b466719ef/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
+google.golang.org/genproto v0.0.0-20220217155828-d576998c0009 h1:8QEZX8dJDqdCxQVLRWzEKGOkOzuDx0AU4+bQX6LwmU4=
+google.golang.org/genproto v0.0.0-20220217155828-d576998c0009/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
 google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=

+ 1 - 1
httpd/handler.go

@@ -190,7 +190,7 @@ func (c *Connection) handleUploadFile(fs vfs.Fs, resolvedPath, filePath, request
 
 	maxWriteSize, _ := c.GetMaxWriteSize(diskQuota, false, fileSize, fs.IsUploadResumeSupported())
 
-	file, w, cancelFn, err := fs.Create(filePath, 0)
+	file, w, cancelFn, err := fs.Create(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC)
 	if err != nil {
 		c.Log(logger.LevelError, "error opening existing file, source: %#v, err: %+v", filePath, err)
 		return nil, c.GetFsError(fs, err)

+ 65 - 1
httpd/httpd.go

@@ -236,6 +236,67 @@ func init() {
 	updateWebClientURLs("")
 }
 
+// HTTPSProxyHeader defines an HTTPS proxy header as key/value.
+// For example Key could be "X-Forwarded-Proto" and Value "https"
+type HTTPSProxyHeader struct {
+	Key   string
+	Value string
+}
+
+// SecurityConf allows to add some security related headers to HTTP responses and to restrict allowed hosts
+type SecurityConf struct {
+	// Set to true to enable the security configurations
+	Enabled bool `json:"enabled" mapstructure:"enabled"`
+	// AllowedHosts is a list of fully qualified domain names that are allowed.
+	// Default is empty list, which allows any and all host names.
+	AllowedHosts []string `json:"allowed_hosts" mapstructure:"allowed_hosts"`
+	// AllowedHostsAreRegex determines if the provided allowed hosts contains valid regular expressions
+	AllowedHostsAreRegex bool `json:"allowed_hosts_are_regex" mapstructure:"allowed_hosts_are_regex"`
+	// HostsProxyHeaders is a set of header keys that may hold a proxied hostname value for the request.
+	HostsProxyHeaders []string `json:"hosts_proxy_headers" mapstructure:"hosts_proxy_headers"`
+	// HTTPSProxyHeaders is a list of header keys with associated values that would indicate a valid https request.
+	HTTPSProxyHeaders []HTTPSProxyHeader `json:"https_proxy_headers" mapstructure:"https_proxy_headers"`
+	// STSSeconds is the max-age of the Strict-Transport-Security header.
+	// Default is 0, which would NOT include the header.
+	STSSeconds int64 `json:"sts_seconds" mapstructure:"sts_seconds"`
+	// If STSIncludeSubdomains is set to true, the "includeSubdomains" will be appended to the
+	// Strict-Transport-Security header. Default is false.
+	STSIncludeSubdomains bool `json:"sts_include_subdomains" mapstructure:"sts_include_subdomains"`
+	// If STSPreload is set to true, the `preload` flag will be appended to the
+	// Strict-Transport-Security header. Default is false.
+	STSPreload bool `json:"sts_preload" mapstructure:"sts_preload"`
+	// If ContentTypeNosniff is true, adds the X-Content-Type-Options header with the value "nosniff". Default is false.
+	ContentTypeNosniff bool `json:"content_type_nosniff" mapstructure:"content_type_nosniff"`
+	// ContentSecurityPolicy allows to set the Content-Security-Policy header value. Default is "".
+	ContentSecurityPolicy string `json:"content_security_policy" mapstructure:"content_security_policy"`
+	// PermissionsPolicy allows to set the Permissions-Policy header value. Default is "".
+	PermissionsPolicy string `json:"permissions_policy" mapstructure:"permissions_policy"`
+	// CrossOriginOpenerPolicy allows to set the `Cross-Origin-Opener-Policy` header value. Default is "".
+	CrossOriginOpenerPolicy string `json:"cross_origin_opener_policy" mapstructure:"cross_origin_opener_policy"`
+	// ExpectCTHeader allows to set the Expect-CT header value. Default is "".
+	ExpectCTHeader string `json:"expect_ct_header" mapstructure:"expect_ct_header"`
+	proxyHeaders   []string
+}
+
+func (s *SecurityConf) updateProxyHeaders() {
+	if !s.Enabled {
+		s.proxyHeaders = nil
+		return
+	}
+	s.proxyHeaders = s.HostsProxyHeaders
+	for _, httpsProxyHeader := range s.HTTPSProxyHeaders {
+		s.proxyHeaders = append(s.proxyHeaders, httpsProxyHeader.Key)
+	}
+}
+
+func (s *SecurityConf) getHTTPSProxyHeaders() map[string]string {
+	headers := make(map[string]string)
+	for _, httpsProxyHeader := range s.HTTPSProxyHeaders {
+		headers[httpsProxyHeader.Key] = httpsProxyHeader.Value
+	}
+	return headers
+}
+
 // WebClientIntegration defines the configuration for an external Web Client integration
 type WebClientIntegration struct {
 	// Files with these extensions can be sent to the configured URL
@@ -290,7 +351,9 @@ type Binding struct {
 	// extensions using an external tool.
 	WebClientIntegrations []WebClientIntegration `json:"web_client_integrations" mapstructure:"web_client_integrations"`
 	// Defining an OIDC configuration the web admin and web client UI will use OpenID to authenticate users.
-	OIDC             OIDC `json:"oidc" mapstructure:"oidc"`
+	OIDC OIDC `json:"oidc" mapstructure:"oidc"`
+	// Security defines security headers to add to HTTP responses and allows to restrict allowed hosts
+	Security         SecurityConf `json:"security" mapstructure:"security"`
 	allowHeadersFrom []func(net.IP) bool
 }
 
@@ -527,6 +590,7 @@ func (c *Conf) Initialize(configDir string) error {
 			return err
 		}
 		binding.checkWebClientIntegrations()
+		binding.Security.updateProxyHeaders()
 
 		go func(b Binding) {
 			if err := b.OIDC.initialize(); err != nil {

+ 9 - 0
httpd/httpd_test.go

@@ -338,6 +338,15 @@ func TestMain(m *testing.M) {
 	httpdConf := config.GetHTTPDConfig()
 
 	httpdConf.Bindings[0].Port = 8081
+	httpdConf.Bindings[0].Security = httpd.SecurityConf{
+		Enabled: true,
+		HTTPSProxyHeaders: []httpd.HTTPSProxyHeader{
+			{
+				Key:   "X-Forwarded-Proto",
+				Value: "https",
+			},
+		},
+	}
 	httpdtest.SetBaseURL(httpBaseURL)
 	backupsPath = filepath.Join(os.TempDir(), "test_backups")
 	httpdConf.BackupsPath = backupsPath

+ 78 - 0
httpd/internal_test.go

@@ -2230,3 +2230,81 @@ func TestBrowsableSharePaths(t *testing.T) {
 		Paths: []string{"/a", "/b"},
 	}
 }
+
+func TestSecureMiddlewareIntegration(t *testing.T) {
+	forwardedHostHeader := "X-Forwarded-Host"
+	server := httpdServer{
+		binding: Binding{
+			ProxyAllowed: []string{"192.168.1.0/24"},
+			Security: SecurityConf{
+				Enabled:              true,
+				AllowedHosts:         []string{"*.sftpgo.com"},
+				AllowedHostsAreRegex: true,
+				HostsProxyHeaders:    []string{forwardedHostHeader},
+				HTTPSProxyHeaders: []HTTPSProxyHeader{
+					{
+						Key:   xForwardedProto,
+						Value: "https",
+					},
+				},
+				STSSeconds:           31536000,
+				STSIncludeSubdomains: true,
+				STSPreload:           true,
+				ContentTypeNosniff:   true,
+			},
+		},
+		enableWebAdmin:  true,
+		enableWebClient: true,
+	}
+	server.binding.Security.updateProxyHeaders()
+	err := server.binding.parseAllowedProxy()
+	assert.NoError(t, err)
+	assert.Equal(t, []string{forwardedHostHeader, xForwardedProto}, server.binding.Security.proxyHeaders)
+	assert.Equal(t, map[string]string{xForwardedProto: "https"}, server.binding.Security.getHTTPSProxyHeaders())
+	server.initializeRouter()
+
+	rr := httptest.NewRecorder()
+	r, err := http.NewRequest(http.MethodGet, webClientLoginPath, nil)
+	assert.NoError(t, err)
+	r.Host = "127.0.0.1"
+	server.router.ServeHTTP(rr, r)
+	assert.Equal(t, http.StatusForbidden, rr.Code)
+
+	rr = httptest.NewRecorder()
+	r.Header.Set(forwardedHostHeader, "www.sftpgo.com")
+	server.router.ServeHTTP(rr, r)
+	assert.Equal(t, http.StatusForbidden, rr.Code)
+	// the header should be removed
+	assert.Empty(t, r.Header.Get(forwardedHostHeader))
+
+	rr = httptest.NewRecorder()
+	r.Host = "test.sftpgo.com"
+	r.Header.Set(forwardedHostHeader, "test.example.com")
+	r.RemoteAddr = "192.168.1.1"
+	server.router.ServeHTTP(rr, r)
+	assert.Equal(t, http.StatusForbidden, rr.Code)
+	assert.NotEmpty(t, r.Header.Get(forwardedHostHeader))
+
+	rr = httptest.NewRecorder()
+	r.Header.Set(forwardedHostHeader, "www.sftpgo.com")
+	r.RemoteAddr = "192.168.1.1"
+	server.router.ServeHTTP(rr, r)
+	assert.Equal(t, http.StatusOK, rr.Code)
+	assert.NotEmpty(t, r.Header.Get(forwardedHostHeader))
+	assert.Empty(t, rr.Header().Get("Strict-Transport-Security"))
+	assert.Equal(t, "nosniff", rr.Header().Get("X-Content-Type-Options"))
+	// now set the X-Forwarded-Proto to https, we should get the Strict-Transport-Security header
+	rr = httptest.NewRecorder()
+	r.Host = "test.sftpgo.com"
+	r.Header.Set(xForwardedProto, "https")
+	r.RemoteAddr = "192.168.1.3"
+	server.router.ServeHTTP(rr, r)
+	assert.Equal(t, http.StatusOK, rr.Code)
+	assert.NotEmpty(t, r.Header.Get(forwardedHostHeader))
+	assert.Equal(t, "max-age=31536000; includeSubDomains; preload", rr.Header().Get("Strict-Transport-Security"))
+	assert.Equal(t, "nosniff", rr.Header().Get("X-Content-Type-Options"))
+
+	server.binding.Security.Enabled = false
+	server.binding.Security.updateProxyHeaders()
+	assert.Len(t, server.binding.Security.proxyHeaders, 0)
+}

+ 37 - 0
httpd/server.go

@@ -20,6 +20,7 @@ import (
 	"github.com/rs/cors"
 	"github.com/rs/xid"
 	"github.com/sftpgo/sdk"
+	"github.com/unrolled/secure"
 
 	"github.com/drakkan/sftpgo/v2/common"
 	"github.com/drakkan/sftpgo/v2/dataprovider"
@@ -939,6 +940,7 @@ func (s *httpdServer) checkConnection(next http.Handler) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
 		ip := net.ParseIP(ipAddr)
+		areHeadersAllowed := false
 		if ip != nil {
 			for _, allow := range s.binding.allowHeadersFrom {
 				if allow(ip) {
@@ -951,10 +953,16 @@ func (s *httpdServer) checkConnection(next http.Handler) http.Handler {
 						ctx := context.WithValue(r.Context(), forwardedProtoKey, forwardedProto)
 						r = r.WithContext(ctx)
 					}
+					areHeadersAllowed = true
 					break
 				}
 			}
 		}
+		if !areHeadersAllowed {
+			for idx := range s.binding.Security.proxyHeaders {
+				r.Header.Del(s.binding.Security.proxyHeaders[idx])
+			}
+		}
 
 		common.Connections.AddClientConnection(ipAddr)
 		defer common.Connections.RemoveClientConnection(ipAddr)
@@ -1008,6 +1016,17 @@ func (s *httpdServer) sendForbiddenResponse(w http.ResponseWriter, r *http.Reque
 	sendAPIResponse(w, r, errors.New(message), message, http.StatusForbidden)
 }
 
+func (s *httpdServer) badHostHandler(w http.ResponseWriter, r *http.Request) {
+	host := r.Host
+	for _, header := range s.binding.Security.HostsProxyHeaders {
+		if h := r.Header.Get(header); h != "" {
+			host = h
+			break
+		}
+	}
+	s.sendForbiddenResponse(w, r, fmt.Sprintf("The host %#v is not allowed", host))
+}
+
 func (s *httpdServer) redirectToWebPath(w http.ResponseWriter, r *http.Request, webPath string) {
 	if dataprovider.HasAdmin() {
 		http.Redirect(w, r, webPath, http.StatusFound)
@@ -1037,6 +1056,24 @@ func (s *httpdServer) initializeRouter() {
 	s.router.Use(s.checkConnection)
 	s.router.Use(logger.NewStructuredLogger(logger.GetLogger()))
 	s.router.Use(middleware.Recoverer)
+	if s.binding.Security.Enabled {
+		secureMiddleware := secure.New(secure.Options{
+			AllowedHosts:            s.binding.Security.AllowedHosts,
+			AllowedHostsAreRegex:    s.binding.Security.AllowedHostsAreRegex,
+			HostsProxyHeaders:       s.binding.Security.HostsProxyHeaders,
+			SSLProxyHeaders:         s.binding.Security.getHTTPSProxyHeaders(),
+			STSSeconds:              s.binding.Security.STSSeconds,
+			STSIncludeSubdomains:    s.binding.Security.STSIncludeSubdomains,
+			STSPreload:              s.binding.Security.STSPreload,
+			ContentTypeNosniff:      s.binding.Security.ContentTypeNosniff,
+			ContentSecurityPolicy:   s.binding.Security.ContentSecurityPolicy,
+			PermissionsPolicy:       s.binding.Security.PermissionsPolicy,
+			CrossOriginOpenerPolicy: s.binding.Security.CrossOriginOpenerPolicy,
+			ExpectCTHeader:          s.binding.Security.ExpectCTHeader,
+		})
+		secureMiddleware.SetBadHostHandler(http.HandlerFunc(s.badHostHandler))
+		s.router.Use(secureMiddleware.Handler)
+	}
 	if s.cors.Enabled {
 		c := cors.New(cors.Options{
 			AllowedOrigins:   s.cors.AllowedOrigins,

+ 3 - 2
sftpd/handler.go

@@ -360,7 +360,7 @@ func (c *Connection) handleSFTPUploadToNewFile(fs vfs.Fs, pflags sftp.FileOpenFl
 	osFlags := getOSOpenFlags(pflags)
 	file, w, cancelFn, err := fs.Create(filePath, osFlags)
 	if err != nil {
-		c.Log(logger.LevelError, "error creating file %#v: %+v", resolvedPath, err)
+		c.Log(logger.LevelError, "error creating file %#vm os flags %v, pflags %+v: %+v", resolvedPath, osFlags, pflags, err)
 		return nil, c.GetFsError(fs, err)
 	}
 
@@ -416,7 +416,8 @@ func (c *Connection) handleSFTPUploadToExistingFile(fs vfs.Fs, pflags sftp.FileO
 
 	file, w, cancelFn, err := fs.Create(filePath, osFlags)
 	if err != nil {
-		c.Log(logger.LevelError, "error opening existing file, flags: %v, source: %#v, err: %+v", pflags, filePath, err)
+		c.Log(logger.LevelError, "error opening existing file, os flags %v, pflags: %+v, source: %#v, err: %+v",
+			osFlags, pflags, filePath, err)
 		return nil, c.GetFsError(fs, err)
 	}
 

+ 1 - 1
sftpd/scp.go

@@ -230,7 +230,7 @@ func (c *scpCommand) handleUploadFile(fs vfs.Fs, resolvedPath, filePath string,
 
 	maxWriteSize, _ := c.connection.GetMaxWriteSize(diskQuota, false, fileSize, fs.IsUploadResumeSupported())
 
-	file, w, cancelFn, err := fs.Create(filePath, 0)
+	file, w, cancelFn, err := fs.Create(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC)
 	if err != nil {
 		c.connection.Log(logger.LevelError, "error creating file %#v: %v", resolvedPath, err)
 		c.sendErrorMessage(fs, err)

+ 15 - 0
sftpgo.json

@@ -227,6 +227,21 @@
           "redirect_base_url": "",
           "username_field": "",
           "role_field": ""
+        },
+        "security": {
+          "enabled": false,
+          "allowed_hosts": [],
+          "allowed_hosts_are_regex": false,
+          "hosts_proxy_headers": [],
+          "https_proxy_headers": [],
+          "sts_seconds": 0,
+          "sts_include_subdomains": false,
+          "sts_preload": false,
+          "content_type_nosniff": false,
+          "content_security_policy": "",
+          "permissions_policy": "",
+          "cross_origin_opener_policy": "",
+          "expect_ct_header": ""
         }
       }
     ],

+ 2 - 2
webdavd/handler.go

@@ -202,7 +202,7 @@ func (c *Connection) handleUploadToNewFile(fs vfs.Fs, resolvedPath, filePath, re
 		c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
 		return nil, c.GetPermissionDeniedError()
 	}
-	file, w, cancelFn, err := fs.Create(filePath, 0)
+	file, w, cancelFn, err := fs.Create(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC)
 	if err != nil {
 		c.Log(logger.LevelError, "error creating file %#v: %+v", resolvedPath, err)
 		return nil, c.GetFsError(fs, err)
@@ -247,7 +247,7 @@ func (c *Connection) handleUploadToExistingFile(fs vfs.Fs, resolvedPath, filePat
 		}
 	}
 
-	file, w, cancelFn, err := fs.Create(filePath, 0)
+	file, w, cancelFn, err := fs.Create(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC)
 	if err != nil {
 		c.Log(logger.LevelError, "error creating file %#v: %+v", resolvedPath, err)
 		return nil, c.GetFsError(fs, err)