From ea6999cdddec209fb9a6f17f0a72f1a12d8bf298 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Mon, 21 Sep 2026 21:37:32 +0800 Subject: [PATCH] security: enforce recursive delete authorization --- chfs-ng/internal/api/path_security_test.go | 24 +++++++++++++++++++++- chfs-ng/internal/api/server.go | 4 ++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/chfs-ng/internal/api/path_security_test.go b/chfs-ng/internal/api/path_security_test.go index 6f20193..6f0c65a 100644 --- a/chfs-ng/internal/api/path_security_test.go +++ b/chfs-ng/internal/api/path_security_test.go @@ -1,6 +1,28 @@ package api -import "testing" +import ( + "os" + "path/filepath" + "testing" + + "chfs-ng/internal/auth" + "chfs-ng/internal/storage" +) + +func TestCheckDeleteTreeRejectsDeniedDescendant(t *testing.T) { + root := t.TempDir() + if err := os.MkdirAll(filepath.Join(root, "public", "private"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(root, "public", "private", "secret.txt"), []byte("x"), 0o644); err != nil { + t.Fatal(err) + } + store := storage.Local{Root: root} + account := &auth.Account{Mask: auth.Delete, Rules: []auth.PathRule{{Dir: "public/private", Mask: auth.None}}} + if err := checkDeleteTree(store, account, "public"); err == nil { + t.Fatal("expected denied descendant to block recursive deletion") + } +} func TestUnsafePath(t *testing.T) { cases := map[string]bool{ diff --git a/chfs-ng/internal/api/server.go b/chfs-ng/internal/api/server.go index 768a81f..a49ff2c 100644 --- a/chfs-ng/internal/api/server.go +++ b/chfs-ng/internal/api/server.go @@ -336,6 +336,10 @@ func (s *Server) remove(w http.ResponseWriter, r *http.Request) { writeJSONError(w, http.StatusForbidden, "has no privilege") return } + if e := checkDeleteTree(s.Store, a, p); e != nil { + writeJSONError(w, http.StatusForbidden, "has no privilege") + return + } if e := s.removePath(p); e != nil { http.Error(w, e.Error(), 500) return