fix: enforce local removal mappings

This commit is contained in:
hetaoBackend 2026-08-06 21:04:22 +08:00
parent e75981a873
commit d38063b49c
2 changed files with 72 additions and 0 deletions

View File

@ -804,3 +804,26 @@ class LocalSandbox(Sandbox):
except OSError as e:
# Re-raise with the original path for clearer error messages, hiding internal resolved paths
raise type(e)(e.errno, e.strerror, path) from None
def remove_file(self, path: str) -> None:
mapping_match = self._find_path_mapping(path)
if mapping_match is None:
resolved = ResolvedPath(str(path), None)
else:
mapping, relative = mapping_match
local_root = Path(self._resolved_local_paths[mapping])
candidate = local_root / relative if relative else local_root
try:
candidate.parent.resolve().relative_to(local_root)
except ValueError as exc:
raise PermissionError(errno.EACCES, "Access denied: path escapes mounted directory", path) from exc
resolved = ResolvedPath(str(candidate), mapping)
if self._is_resolved_path_read_only(resolved):
raise OSError(errno.EROFS, "Read-only file system", path)
try:
os.unlink(resolved.path)
except FileNotFoundError:
return
except OSError as exc:
raise type(exc)(exc.errno, exc.strerror, path) from None

View File

@ -182,8 +182,57 @@ class TestReadOnlyPath:
sandbox.update_file("/mnt/skills/existing.py", b"updated")
assert exc_info.value.errno == errno.EROFS
def test_remove_file_blocked_on_read_only(self, tmp_path):
skills_dir = tmp_path / "skills"
skills_dir.mkdir()
existing_file = skills_dir / "existing.py"
existing_file.write_bytes(b"original")
sandbox = LocalSandbox(
"test",
[PathMapping(container_path="/mnt/skills", local_path=str(skills_dir), read_only=True)],
)
with pytest.raises(OSError) as exc_info:
sandbox.remove_file("/mnt/skills/existing.py")
assert exc_info.value.errno == errno.EROFS
assert existing_file.read_bytes() == b"original"
def test_remove_file_deletes_exact_writable_path(self, tmp_path):
data_dir = tmp_path / "data"
data_dir.mkdir()
existing_file = data_dir / "existing.txt"
existing_file.write_bytes(b"payload")
sandbox = LocalSandbox(
"test",
[PathMapping(container_path="/mnt/data", local_path=str(data_dir), read_only=False)],
)
sandbox.remove_file("/mnt/data/existing.txt")
assert not existing_file.exists()
class TestSymlinkEscapes:
def test_remove_file_blocks_symlinked_parent_escape(self, tmp_path):
mount_dir = tmp_path / "mount"
mount_dir.mkdir()
outside_dir = tmp_path / "outside"
outside_dir.mkdir()
victim = outside_dir / "victim.txt"
victim.write_text("protected", encoding="utf-8")
_symlink_to(outside_dir, mount_dir / "escape", target_is_directory=True)
sandbox = LocalSandbox(
"test",
[PathMapping(container_path="/mnt/data", local_path=str(mount_dir), read_only=False)],
)
with pytest.raises(PermissionError) as exc_info:
sandbox.remove_file("/mnt/data/escape/victim.txt")
assert exc_info.value.errno == errno.EACCES
assert victim.read_text(encoding="utf-8") == "protected"
def test_read_file_blocks_symlink_escape_from_mount(self, tmp_path):
mount_dir = tmp_path / "mount"
mount_dir.mkdir()