fix(security): fix file_ops.py validate_sandbox_path startswith bypass #7478
CI / lint (pull_request) Successful in 1m12s
CI / typecheck (pull_request) Successful in 1m27s
CI / security (pull_request) Successful in 1m25s
CI / push-validation (pull_request) Successful in 41s
CI / helm (pull_request) Successful in 45s
CI / build (pull_request) Successful in 55s
CI / quality (pull_request) Successful in 1m40s
CI / integration_tests (pull_request) Successful in 7m3s
CI / unit_tests (pull_request) Successful in 8m54s
CI / docker (pull_request) Successful in 1m58s
CI / coverage (pull_request) Successful in 13m17s
CI / status-check (pull_request) Successful in 4s

Replace string-based startswith() path traversal check in validate_sandbox_path
with robust Path.is_relative_to(). The old check using str(target).startswith(str(root))
could be evaded by paths like /workdir/sandboxed/secret when root is /workdir/sandbox,
because the malicious path happens to start with the root string.

Path.is_relative_to() uses semantic path containment comparison and correctly
rejects /workdir/sandboxed/secret as escaping the sandbox at /workdir/sandbox.

Also added a docstring explaining the vulnerability pattern.

ISSUES CLOSED: #7478
This commit is contained in:
2026-05-16 09:34:40 +00:00
parent 5c5309f35d
commit f37e4104de
+6 -9
View File
@@ -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