diff --git a/src/cleveragents/skills/builtins/file_ops.py b/src/cleveragents/skills/builtins/file_ops.py index 7b02b5484..11281635d 100644 --- a/src/cleveragents/skills/builtins/file_ops.py +++ b/src/cleveragents/skills/builtins/file_ops.py @@ -70,9 +70,10 @@ def validate_sandbox_path(path_str: str, sandbox_root: str | None = None) -> Pat Raises ``ValueError`` with the rejected path when traversal is detected or the resolved path falls outside the sandbox root. - Uses :meth:`Path.relative_to` (not string prefix matching) to avoid - the *prefix-collision bypass*: a target like ``/tmp/abc123-escape`` - would incorrectly pass ``startswith("/tmp/abc123")``. + Uses :meth:`Path.is_relative_to` for robust, semantic path containment + checks — string-based ``startswith()`` guards are vulnerable to bypasses + when one sandbox name is a prefix of another (e.g., ``/tmp/sandbox`` + incorrectly passing for ``/tmp/sandbox_evil/file.txt``). """ if not path_str: raise ValueError("Path must not be empty") @@ -81,12 +82,8 @@ def validate_sandbox_path(path_str: str, sandbox_root: str | None = None) -> Pat root = root.resolve() target = (root / path_str).resolve() - try: - target.relative_to(root) - except ValueError as exc: - raise ValueError( - f"Path traversal detected: '{path_str}' escapes sandbox root" - ) from exc + if not target.is_relative_to(root): + raise ValueError(f"Path traversal detected: '{path_str}' escapes sandbox root") return target