diff --git a/config/config.go b/config/config.go index 1f666eac..321fdd6d 100644 --- a/config/config.go +++ b/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) } diff --git a/config/config_test.go b/config/config_test.go index 2bd594ca..9eb7b251 100644 --- a/config/config_test.go +++ b/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) { diff --git a/docs/full-configuration.md b/docs/full-configuration.md index edac2e85..2186e066 100644 --- a/docs/full-configuration.md +++ b/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. diff --git a/ftpd/handler.go b/ftpd/handler.go index dace0cac..65083328 100644 --- a/ftpd/handler.go +++ b/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) } diff --git a/go.mod b/go.mod index 1b639aea..267fcd8d 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 7c167abe..df00ff06 100644 --- a/go.sum +++ b/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= diff --git a/httpd/handler.go b/httpd/handler.go index fa56e233..f7e2b45d 100644 --- a/httpd/handler.go +++ b/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) diff --git a/httpd/httpd.go b/httpd/httpd.go index 761c6d9b..bc85a67c 100644 --- a/httpd/httpd.go +++ b/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 { diff --git a/httpd/httpd_test.go b/httpd/httpd_test.go index 2790d87e..9bf64e09 100644 --- a/httpd/httpd_test.go +++ b/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 diff --git a/httpd/internal_test.go b/httpd/internal_test.go index f3650982..b3e62bd4 100644 --- a/httpd/internal_test.go +++ b/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) +} diff --git a/httpd/server.go b/httpd/server.go index e03876c4..6518fd23 100644 --- a/httpd/server.go +++ b/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, diff --git a/sftpd/handler.go b/sftpd/handler.go index 0e5ef0d6..39552a67 100644 --- a/sftpd/handler.go +++ b/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) } diff --git a/sftpd/scp.go b/sftpd/scp.go index 03014ba5..3a501de2 100644 --- a/sftpd/scp.go +++ b/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) diff --git a/sftpgo.json b/sftpgo.json index b00fa3fa..c82c56c6 100644 --- a/sftpgo.json +++ b/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": "" } } ], diff --git a/webdavd/handler.go b/webdavd/handler.go index 35ede235..25d54d71 100644 --- a/webdavd/handler.go +++ b/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)