From 0f071f396bd76a982d1a07dbe8b978e152c4a89e Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Mon, 21 Sep 2026 17:13:07 +0800 Subject: [PATCH] fix: enforce recursive WebDAV permissions --- chfs-ng/internal/api/server.go | 32 ++++++++++++++++++++++++- chfs-ng/internal/api/webdav_test.go | 37 +++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 chfs-ng/internal/api/webdav_test.go diff --git a/chfs-ng/internal/api/server.go b/chfs-ng/internal/api/server.go index 686a2e6..83863d0 100644 --- a/chfs-ng/internal/api/server.go +++ b/chfs-ng/internal/api/server.go @@ -829,6 +829,9 @@ func (f *webdavAuthFS) RemoveAll(ctx context.Context, name string, opts *webdav. if err := f.allow(name, auth.Delete); err != nil { return err } + if err := f.checkTree(ctx, name, auth.Delete); err != nil { + return err + } return f.inner.RemoveAll(ctx, name, opts) } func (f *webdavAuthFS) Mkdir(ctx context.Context, name string) error { @@ -845,6 +848,9 @@ func (f *webdavAuthFS) Copy(ctx context.Context, name, dest string, opts *webdav if err := f.allow(name, auth.Read); err != nil { return false, err } + if err := f.checkTree(ctx, name, auth.Read); err != nil { + return false, err + } parent := path.Dir(strings.Trim(dest, "/")) if parent == "." { parent = "" @@ -855,7 +861,10 @@ func (f *webdavAuthFS) Copy(ctx context.Context, name, dest string, opts *webdav return f.inner.Copy(ctx, name, dest, opts) } func (f *webdavAuthFS) Move(ctx context.Context, name, dest string, opts *webdav.MoveOptions) (bool, error) { - if err := f.allow(name, auth.Write); err != nil { + if err := f.allow(name, auth.Delete); err != nil { + return false, err + } + if err := f.checkTree(ctx, name, auth.Delete); err != nil { return false, err } parent := path.Dir(strings.Trim(dest, "/")) @@ -868,6 +877,27 @@ func (f *webdavAuthFS) Move(ctx context.Context, name, dest string, opts *webdav return f.inner.Move(ctx, name, dest, opts) } +func (f *webdavAuthFS) checkTree(ctx context.Context, name string, p auth.PermMask) error { + info, err := f.inner.Stat(ctx, name) + if err != nil { + return err + } + if !info.IsDir { + return nil + } + items, err := f.inner.ReadDir(ctx, name, true) + if err != nil { + return err + } + for _, item := range items { + itemPath := strings.Trim(item.Path, "/") + if err := f.allow(itemPath, p); err != nil { + return err + } + } + return nil +} + func (s *Server) ipFilter(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if s.Allow != "" && !allowedIP(s.Allow, clientIP(r)) { diff --git a/chfs-ng/internal/api/webdav_test.go b/chfs-ng/internal/api/webdav_test.go new file mode 100644 index 0000000..332234f --- /dev/null +++ b/chfs-ng/internal/api/webdav_test.go @@ -0,0 +1,37 @@ +package api + +import ( + "context" + "os" + "path/filepath" + "testing" + + "chfs-ng/internal/auth" + "github.com/emersion/go-webdav" +) + +func TestWebDAVAuthFSCheckTree(t *testing.T) { + root := t.TempDir() + if err := os.MkdirAll(filepath.Join(root, "test", "private"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(root, "test", "public.txt"), []byte("public"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(root, "test", "private", "secret.txt"), []byte("secret"), 0o644); err != nil { + t.Fatal(err) + } + + account := &auth.Account{Mask: auth.Delete, Rules: []auth.PathRule{ + {Dir: "test", Mask: auth.Delete}, + {Dir: "test/private", Mask: auth.None}, + }} + fs := &webdavAuthFS{inner: webdav.LocalFileSystem(root), account: account} + + if err := fs.checkTree(context.Background(), "/test", auth.Delete); err == nil { + t.Fatal("expected recursive delete permission check to reject inaccessible descendant") + } + if err := fs.checkTree(context.Background(), "/test/public.txt", auth.Delete); err != nil { + t.Fatalf("public file should be deletable: %v", err) + } +}