security: harden recursive file operations

This commit is contained in:
OpenClaw Agent
2026-09-21 21:46:11 +08:00
parent ea6999cddd
commit ece101edd7
4 changed files with 89 additions and 2 deletions
+18
View File
@@ -326,6 +326,7 @@ func (s *Server) remove(w http.ResponseWriter, r *http.Request) {
if len(items) == 0 {
items = r.Form["path"]
}
paths := make([]string, 0, len(items))
for _, raw := range items {
if unsafePath(raw) {
writeJSONError(w, http.StatusBadRequest, "invalid parameter")
@@ -340,6 +341,11 @@ func (s *Server) remove(w http.ResponseWriter, r *http.Request) {
writeJSONError(w, http.StatusForbidden, "has no privilege")
return
}
paths = append(paths, p)
}
// Preflight every target before mutating anything, so a multi-file delete
// cannot partially succeed when a later item is forbidden.
for _, p := range paths {
if e := s.removePath(p); e != nil {
http.Error(w, e.Error(), 500)
return
@@ -881,6 +887,12 @@ func davSafePath(name string) (string, error) {
return p, nil
}
func sameOrDescendantPath(src, dst string) bool {
src = strings.Trim(path.Clean("/"+src), "/")
dst = strings.Trim(path.Clean("/"+dst), "/")
return src == dst || (src != "" && strings.HasPrefix(dst, src+"/"))
}
func (f *webdavStorageFS) info(name string) (*webdav.FileInfo, error) {
p, err := davSafePath(name)
if err != nil {
@@ -1023,6 +1035,9 @@ func (f *webdavStorageFS) Copy(ctx context.Context, name, dest string, opts *web
if err != nil {
return false, err
}
if sameOrDescendantPath(src, dst) {
return false, webdav.NewHTTPError(http.StatusConflict, fmt.Errorf("destination is inside source"))
}
if opts != nil && opts.NoOverwrite {
if _, err := f.store.Stat(dst); err == nil {
return false, webdav.NewHTTPError(http.StatusPreconditionFailed, fmt.Errorf("destination exists"))
@@ -1088,6 +1103,9 @@ func (f *webdavStorageFS) Move(ctx context.Context, name, dest string, opts *web
if err != nil {
return false, err
}
if sameOrDescendantPath(src, dst) {
return false, webdav.NewHTTPError(http.StatusConflict, fmt.Errorf("destination is inside source"))
}
if opts == nil || opts.NoOverwrite {
if _, err := f.store.Stat(dst); err == nil {
return false, webdav.NewHTTPError(http.StatusPreconditionFailed, fmt.Errorf("destination exists"))
+20
View File
@@ -74,6 +74,26 @@ func TestSameWebDAVDestinationHost(t *testing.T) {
}
}
func TestWebDAVStorageFSRejectsDestinationInsideSource(t *testing.T) {
root := t.TempDir()
if err := os.MkdirAll(filepath.Join(root, "src", "child"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, "src", "file.txt"), []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
fs := &webdavStorageFS{store: storage.Local{Root: root}}
if _, err := fs.Copy(context.Background(), "/src", "/src/child/copy", &webdav.CopyOptions{}); err == nil {
t.Fatal("copying a directory into its descendant should fail")
}
if _, err := fs.Move(context.Background(), "/src", "/src/child/moved", &webdav.MoveOptions{}); err == nil {
t.Fatal("moving a directory into its descendant should fail")
}
if _, err := fs.Copy(context.Background(), "/src", "/src", &webdav.CopyOptions{}); err == nil {
t.Fatal("copying a resource onto itself should fail")
}
}
func TestWebDAVStorageFSCopyAndMove(t *testing.T) {
root1 := t.TempDir()
root2 := t.TempDir()