Skip to content

Commit

Permalink
WebClient: do not silently overwrite files/directories
Browse files Browse the repository at this point in the history
Signed-off-by: Nicola Murino <[email protected]>
  • Loading branch information
drakkan committed Dec 28, 2023
1 parent e35e07a commit 3121c35
Show file tree
Hide file tree
Showing 20 changed files with 564 additions and 90 deletions.
1 change: 0 additions & 1 deletion docs/full-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,6 @@ The configuration file contains the following sections:
- `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.
- `branding`, struct. Defines the supported customizations to suit your brand. It contains the `web_admin` and `web_client` structs that define customizations for the WebAdmin and the WebClient UIs. Each customization struct contains the following fields:
- `name`, string. Defines the UI name
- `short_name`, string. Defines the short name to show next to the logo image and on the login page
Expand Down
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ require (
github.com/pires/go-proxyproto v0.7.0
github.com/pkg/sftp v1.13.6
github.com/pquerna/otp v1.4.0
github.com/prometheus/client_golang v1.17.0
github.com/prometheus/client_golang v1.18.0
github.com/robfig/cron/v3 v3.0.1
github.com/rs/cors v1.10.1
github.com/rs/xid v1.5.0
Expand All @@ -61,13 +61,13 @@ require (
github.com/stretchr/testify v1.8.4
github.com/studio-b12/gowebdav v0.9.0
github.com/subosito/gotenv v1.6.0
github.com/unrolled/secure v1.13.0
github.com/unrolled/secure v1.14.0
github.com/wagslane/go-password-validator v0.3.0
github.com/wneessen/go-mail v0.4.1-0.20230815095916-0189acf1e45f
github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a
go.etcd.io/bbolt v1.3.8
go.uber.org/automaxprocs v1.5.3
gocloud.dev v0.35.0
gocloud.dev v0.36.0
golang.org/x/crypto v0.17.0
golang.org/x/net v0.19.0
golang.org/x/oauth2 v0.15.0
Expand Down
12 changes: 6 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -327,8 +327,8 @@ github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
Expand Down Expand Up @@ -397,8 +397,8 @@ github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5I
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4=
github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY=
github.com/unrolled/secure v1.13.0 h1:sdr3Phw2+f8Px8HE5sd1EHdj1aV3yUwed/uZXChLFsk=
github.com/unrolled/secure v1.13.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
github.com/unrolled/secure v1.14.0 h1:u9vJTU/pR4Bny0ntLUMxdfLtmIRGvQf2sEFuA0TG9AE=
github.com/unrolled/secure v1.14.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/wneessen/go-mail v0.4.1-0.20230815095916-0189acf1e45f h1:IYzF42VUzA6es43UO0q8rdB1+d7fge5ALPOVKN192jA=
Expand Down Expand Up @@ -429,8 +429,8 @@ go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
gocloud.dev v0.35.0 h1:x/Gtt5OJdT4j+ir1AXAIXb7bBnFawXAAaJptCUGk3HU=
gocloud.dev v0.35.0/go.mod h1:wbyF+BhfdtLWyUtVEWRW13hFLb1vXnV2ovEhYGQe3ck=
gocloud.dev v0.36.0 h1:q5zoXux4xkOZP473e1EZbG8Gq9f0vlg1VNH5Du/ybus=
gocloud.dev v0.36.0/go.mod h1:bLxah6JQVKBaIxzsr5BQLYB4IYdWHkMZdzCXlo6F0gg=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20231226003508-02704c960a9b h1:kLiC65FbiHWFAOu+lxwNPujcsl8VYyTYYEZnsOO1WK4=
golang.org/x/exp v0.0.0-20231226003508-02704c960a9b/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
Expand Down
2 changes: 1 addition & 1 deletion internal/common/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -1164,7 +1164,7 @@ func (c *BaseConnection) isRenamePermitted(fsSrc, fsDst vfs.Fs, fsSourcePath, fs
virtualTargetPath string, fi os.FileInfo,
) bool {
if !c.IsSameResource(virtualSourcePath, virtualTargetPath) {
c.Log(logger.LevelInfo, "rename %#q->%q is not allowed: the paths must be on the same resource",
c.Log(logger.LevelInfo, "rename %q->%q is not allowed: the paths must be on the same resource",
virtualSourcePath, virtualTargetPath)
return false
}
Expand Down
7 changes: 0 additions & 7 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,6 @@ var (
ContentSecurityPolicy: "",
PermissionsPolicy: "",
CrossOriginOpenerPolicy: "",
ExpectCTHeader: "",
},
Branding: httpd.Branding{},
}
Expand Down Expand Up @@ -1542,12 +1541,6 @@ func getHTTPDSecurityConfFromEnv(idx int) (httpd.SecurityConf, bool) { //nolint:
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
}

Expand Down
3 changes: 0 additions & 3 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1237,7 +1237,6 @@ func TestHTTPDBindingsFromEnv(t *testing.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"`)
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__0__PATH", "path1")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__1__PATH", "path2")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__FAVICON_PATH", "favicon.ico")
Expand Down Expand Up @@ -1303,7 +1302,6 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
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")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__0__PATH")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__1__PATH")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__FAVICON_PATH")
Expand Down Expand Up @@ -1414,7 +1412,6 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
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)
require.Equal(t, "favicon.ico", bindings[2].Branding.WebAdmin.FaviconPath)
require.Equal(t, "logo.png", bindings[2].Branding.WebClient.LogoPath)
require.Equal(t, "login_image.png", bindings[2].Branding.WebAdmin.LoginImagePath)
Expand Down
17 changes: 10 additions & 7 deletions internal/httpd/api_shares.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ func (s *httpdServer) readBrowsableShareContents(w http.ResponseWriter, r *http.
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
name, err := getBrowsableSharedPath(share, r)
name, err := getBrowsableSharedPath(share.Paths[0], r)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
Expand Down Expand Up @@ -232,7 +232,7 @@ func (s *httpdServer) downloadBrowsableSharedFile(w http.ResponseWriter, r *http
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
name, err := getBrowsableSharedPath(share, r)
name, err := getBrowsableSharedPath(share.Paths[0], r)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
Expand Down Expand Up @@ -552,7 +552,10 @@ func validateBrowsableShare(share dataprovider.Share, connection *Connection) er
basePath := share.Paths[0]
info, err := connection.Stat(basePath, 0)
if err != nil {
return fmt.Errorf("unable to check the share directory: %w", err)
return util.NewI18nError(
fmt.Errorf("unable to check the share directory: %w", err),
util.I18nErrorShareInvalidPath,
)
}
if !info.IsDir() {
return util.NewI18nError(
Expand All @@ -563,12 +566,12 @@ func validateBrowsableShare(share dataprovider.Share, connection *Connection) er
return nil
}

func getBrowsableSharedPath(share dataprovider.Share, r *http.Request) (string, error) {
name := util.CleanPath(path.Join(share.Paths[0], r.URL.Query().Get("path")))
if share.Paths[0] == "/" {
func getBrowsableSharedPath(shareBasePath string, r *http.Request) (string, error) {
name := util.CleanPath(path.Join(shareBasePath, r.URL.Query().Get("path")))
if shareBasePath == "/" {
return name, nil
}
if name != share.Paths[0] && !strings.HasPrefix(name, share.Paths[0]+"/") {
if name != shareBasePath && !strings.HasPrefix(name, shareBasePath+"/") {
return "", util.NewI18nError(
util.NewValidationError(fmt.Sprintf("Invalid path %q", r.URL.Query().Get("path"))),
util.I18nErrorPathInvalid,
Expand Down
4 changes: 2 additions & 2 deletions internal/httpd/api_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,11 @@ func getRespStatus(err error) int {
func getMappedStatusCode(err error) int {
var statusCode int
switch {
case errors.Is(err, os.ErrPermission):
case errors.Is(err, fs.ErrPermission):
statusCode = http.StatusForbidden
case errors.Is(err, common.ErrReadQuotaExceeded):
statusCode = http.StatusForbidden
case errors.Is(err, os.ErrNotExist):
case errors.Is(err, fs.ErrNotExist):
statusCode = http.StatusNotFound
case errors.Is(err, common.ErrQuotaExceeded):
statusCode = http.StatusRequestEntityTooLarge
Expand Down
7 changes: 4 additions & 3 deletions internal/httpd/httpd.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ const (
webClientResetPwdPathDefault = "/web/client/reset-password"
webClientViewPDFPathDefault = "/web/client/viewpdf"
webClientGetPDFPathDefault = "/web/client/getpdf"
webClientExistPathDefault = "/web/client/exist"
webStaticFilesPathDefault = "/static"
webOpenAPIPathDefault = "/openapi"
// MaxRestoreSize defines the max size for the loaddata input file
Expand Down Expand Up @@ -278,6 +279,7 @@ var (
webClientResetPwdPath string
webClientViewPDFPath string
webClientGetPDFPath string
webClientExistPath string
webStaticFilesPath string
webOpenAPIPath string
// max upload size for http clients, 1GB by default
Expand Down Expand Up @@ -341,9 +343,7 @@ type SecurityConf struct {
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
proxyHeaders []string
}

func (s *SecurityConf) updateProxyHeaders() {
Expand Down Expand Up @@ -1110,6 +1110,7 @@ func updateWebClientURLs(baseURL string) {
webClientResetPwdPath = path.Join(baseURL, webClientResetPwdPathDefault)
webClientViewPDFPath = path.Join(baseURL, webClientViewPDFPathDefault)
webClientGetPDFPath = path.Join(baseURL, webClientGetPDFPathDefault)
webClientExistPath = path.Join(baseURL, webClientExistPathDefault)
}

func updateWebAdminURLs(baseURL string) {
Expand Down
Loading

0 comments on commit 3121c35

Please sign in to comment.