httpd: allow to restrict allowed hosts ...
... and to add security headers to the responses Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
876bf8aa4f
commit
f1a255aa6c
15 changed files with 415 additions and 24 deletions
121
config/config.go
121
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
9
go.mod
9
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
|
||||
|
|
17
go.sum
17
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=
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
@ -291,6 +352,8 @@ type Binding struct {
|
|||
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"`
|
||||
// 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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
sftpgo.json
15
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": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue