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
+28 -2
View File
@@ -97,10 +97,36 @@ func (s Local) Create(path string) (*os.File, error) {
}
func (s Local) Remove(path string) error {
p, e := s.real(path)
if e != nil {
if e == nil {
return os.RemoveAll(p)
}
// A final symlink that points outside the root is still safe to delete:
// remove the link itself, but never follow it to its target. Validate its
// parent through real() so nested symlink escapes remain rejected.
clean := strings.TrimPrefix(filepath.Clean("/"+path), string(filepath.Separator))
if clean == "" {
return e
}
return os.RemoveAll(p)
parent := filepath.ToSlash(filepath.Dir(clean))
if parent == "." {
parent = ""
}
if _, parentErr := s.real(parent); parentErr != nil {
return e
}
root, rootErr := filepath.Abs(s.Root)
if rootErr != nil {
return e
}
root, rootErr = filepath.EvalSymlinks(root)
if rootErr != nil {
return e
}
candidate := filepath.Join(root, filepath.FromSlash(clean))
if info, lstatErr := os.Lstat(candidate); lstatErr == nil && info.Mode()&os.ModeSymlink != 0 {
return os.Remove(candidate)
}
return e
}
func (s Local) Trash(path string) error {
+23
View File
@@ -7,6 +7,29 @@ import (
"time"
)
func TestLocalRemoveSymlinkDoesNotDeleteTarget(t *testing.T) {
root := t.TempDir()
outside := t.TempDir()
target := filepath.Join(outside, "secret.txt")
if err := os.WriteFile(target, []byte("keep"), 0o644); err != nil {
t.Fatal(err)
}
link := filepath.Join(root, "link")
if err := os.Symlink(target, link); err != nil {
t.Skipf("symlink unavailable: %v", err)
}
store := Local{Root: root}
if err := store.Remove("link"); err != nil {
t.Fatalf("remove symlink: %v", err)
}
if _, err := os.Stat(target); err != nil {
t.Fatalf("symlink removal must not delete target: %v", err)
}
if _, err := os.Lstat(link); !os.IsNotExist(err) {
t.Fatalf("symlink should be removed, got %v", err)
}
}
func TestLocalRejectsSymlinkEscapeAndAllowsNestedCreate(t *testing.T) {
root := t.TempDir()
outside := t.TempDir()