From 9ab1df54cd5471ced7e898b7652c97980e61d191 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Sat, 4 Jan 2025 18:43:05 +0100 Subject: [PATCH] fix: rename/copy when using prefix --- lib/handler.go | 48 ++++++++++++++++++++++------ lib/handler_test.go | 76 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 9 deletions(-) diff --git a/lib/handler.go b/lib/handler.go index bc572cc..61814e2 100644 --- a/lib/handler.go +++ b/lib/handler.go @@ -19,6 +19,7 @@ type handlerUser struct { type Handler struct { noPassword bool behindProxy bool + prefix string user *handlerUser users map[string]*handlerUser } @@ -27,12 +28,12 @@ func NewHandler(c *Config) (http.Handler, error) { h := &Handler{ noPassword: c.NoPassword, behindProxy: c.BehindProxy, + prefix: c.Prefix, user: &handlerUser{ User: User{ UserPermissions: c.UserPermissions, }, Handler: webdav.Handler{ - Prefix: c.Prefix, FileSystem: Dir{ Dir: webdav.Dir(c.Directory), noSniff: c.NoSniff, @@ -47,7 +48,6 @@ func NewHandler(c *Config) (http.Handler, error) { h.users[u.Username] = &handlerUser{ User: u, Handler: webdav.Handler{ - Prefix: c.Prefix, FileSystem: Dir{ Dir: webdav.Dir(u.Directory), noSniff: c.NoSniff, @@ -116,19 +116,49 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { zap.L().Info("user authorized", zap.String("username", username), zap.String("remote_address", remoteAddr)) } - // Cleanup destination header if it's present by stripping out the prefix - // and only keeping the path. + // Validate and clean destination header if it exists, by stripping out the + // prefix and only keeping the actual destination path, always prefixed by + // a forward slash to ensure that the rules can successful match the path. if destination := r.Header.Get("Destination"); destination != "" { u, err := url.Parse(destination) - if err == nil { - destination = strings.TrimPrefix(u.Path, user.Prefix) - if !strings.HasPrefix(destination, "/") { - destination = "/" + destination + if err != nil { + http.Error(w, "Invalid Destination header", http.StatusBadRequest) + return + } + + if h.prefix != "" { + destination = strings.TrimPrefix(u.Path, h.prefix) + if len(destination) >= len(u.Path) { + http.Error(w, "Invalid URL prefix", http.StatusBadRequest) + return } - r.Header.Set("Destination", destination) + } + + if !strings.HasPrefix(destination, "/") { + destination = "/" + destination + } + + r.Header.Set("Destination", destination) + } + + // Clean up URL path by stripping out the prefix, and ensuring it always begins + // with a forward slash, so that it can match against the rules. + path := r.URL.Path + + if h.prefix != "" { + path = strings.TrimPrefix(r.URL.Path, h.prefix) + if len(path) >= len(r.URL.Path) { + http.Error(w, "Invalid URL prefix", http.StatusBadRequest) + return } } + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + + r.URL.Path = path + // Checks for user permissions relatively to this PATH. allowed := user.Allowed(r, func(filename string) bool { _, err := user.FileSystem.Stat(r.Context(), filename) diff --git a/lib/handler_test.go b/lib/handler_test.go index 1c8b133..d805e94 100644 --- a/lib/handler_test.go +++ b/lib/handler_test.go @@ -292,6 +292,82 @@ users: require.ErrorContains(t, err, "403") } +func TestServerRulesPrefix(t *testing.T) { + t.Parallel() + + dir := makeTestDirectory(t, map[string][]byte{ + "foo.txt": []byte("foo"), + "bar.js": []byte("foo js"), + "a/foo.js": []byte("foo js"), + "a/foo.txt": []byte("foo txt"), + "b/foo.txt": []byte("foo b"), + "c/a.txt": []byte("b"), + "c/b.txt": []byte("b"), + "c/c.txt": []byte("b"), + }) + + srv := makeTestServer(t, fmt.Sprintf(` +directory: %s +permissions: CRUD +prefix: /prefix + +users: + - username: basic + password: basic + rules: + - regex: "^.+.js$" + permissions: R + - path: "/b/" + permissions: R + - path: "/a/foo.txt" + permissions: none + - path: "/c/" + permissions: none +`, dir)) + + client := gowebdav.NewClient(srv.URL, "basic", "basic") + + files, err := client.ReadDir("/prefix") + require.NoError(t, err) + require.Len(t, files, 5) + + err = client.Write("/prefix/foo.txt", []byte("new"), 0666) + require.NoError(t, err) + + err = client.Write("/prefix/new.txt", []byte("new"), 0666) + require.NoError(t, err) + + err = client.Copy("/prefix/bar.js", "/prefix/b/bar.js", false) + require.ErrorContains(t, err, "403") + + err = client.Copy("/prefix/bar.js", "/prefix/bar.jsx", false) + require.NoError(t, err) + + err = client.Copy("/prefix/b/foo.txt", "/prefix/foo1.txt", false) + require.NoError(t, err) + + err = client.Rename("/prefix/b/foo.txt", "/prefix/foo2.txt", false) + require.ErrorContains(t, err, "403") + + _, err = client.Read("/prefix/a/foo.txt") + require.ErrorContains(t, err, "403") + + err = client.Write("/prefix/a/foo.js", []byte("new"), 0666) + require.ErrorContains(t, err, "403") + + err = client.Write("/prefix/b/foo.txt", []byte("new"), 0666) + require.ErrorContains(t, err, "403") + + _, err = client.ReadDir("/prefix/c") + require.ErrorContains(t, err, "403") + + _, err = client.Read("/prefix/c/a.txt") + require.ErrorContains(t, err, "403") + + err = client.Write("/prefix/c/b.txt", []byte("new"), 0666) + require.ErrorContains(t, err, "403") +} + func TestServerPermissions(t *testing.T) { t.Parallel()