feat(sandbox): restrict risky absolute paths in local bash commands

- validate absolute path usage in local-mode bash commands
- allow only /mnt/user-data virtual paths for user data access
- keep a small allowlist for system executable/device paths
- return clear permission errors for unsafe command paths
- add regression tests for bash path validation rules
This commit is contained in:
Willem Jiang
2026-03-05 22:13:06 +08:00
parent 34e3f5c9d4
commit 24a8ea76ee
2 changed files with 67 additions and 0 deletions

View File

@@ -14,6 +14,16 @@ from src.sandbox.exceptions import (
from src.sandbox.sandbox import Sandbox
from src.sandbox.sandbox_provider import get_sandbox_provider
_ABSOLUTE_PATH_PATTERN = re.compile(r"(?<![:\w])/(?:[^\s\"'`;&|<>()]+)")
_LOCAL_BASH_SYSTEM_PATH_PREFIXES = (
"/bin/",
"/usr/bin/",
"/usr/sbin/",
"/sbin/",
"/opt/homebrew/bin/",
"/dev/",
)
def replace_virtual_path(path: str, thread_data: ThreadDataState | None) -> str:
"""Replace virtual /mnt/user-data paths with actual thread data paths.
@@ -144,6 +154,35 @@ def resolve_local_tool_path(path: str, thread_data: ThreadDataState | None) -> s
raise PermissionError("Access denied: path traversal detected")
def validate_local_bash_command_paths(command: str, thread_data: ThreadDataState | None) -> None:
"""Validate absolute paths in local-sandbox bash commands.
In local mode, commands must use virtual paths under /mnt/user-data for
user data access. A small allowlist of common system path prefixes is kept
for executable and device references (e.g. /bin/sh, /dev/null).
"""
if thread_data is None:
raise SandboxRuntimeError("Thread data not available for local sandbox")
unsafe_paths: list[str] = []
for absolute_path in _ABSOLUTE_PATH_PATTERN.findall(command):
if absolute_path == VIRTUAL_PATH_PREFIX or absolute_path.startswith(f"{VIRTUAL_PATH_PREFIX}/"):
continue
if any(
absolute_path == prefix.rstrip("/") or absolute_path.startswith(prefix)
for prefix in _LOCAL_BASH_SYSTEM_PATH_PREFIXES
):
continue
unsafe_paths.append(absolute_path)
if unsafe_paths:
unsafe = ", ".join(sorted(dict.fromkeys(unsafe_paths)))
raise PermissionError(f"Unsafe absolute paths in command: {unsafe}. Use paths under {VIRTUAL_PATH_PREFIX}")
def replace_virtual_paths_in_command(command: str, thread_data: ThreadDataState | None) -> str:
"""Replace all virtual /mnt/user-data paths in a command string.
@@ -330,12 +369,15 @@ def bash_tool(runtime: ToolRuntime[ContextT, ThreadState], description: str, com
ensure_thread_directories_exist(runtime)
thread_data = get_thread_data(runtime)
if is_local_sandbox(runtime):
validate_local_bash_command_paths(command, thread_data)
command = replace_virtual_paths_in_command(command, thread_data)
output = sandbox.execute_command(command)
return mask_local_paths_in_output(output, thread_data)
return sandbox.execute_command(command)
except SandboxError as e:
return f"Error: {e}"
except PermissionError as e:
return f"Error: {e}"
except Exception as e:
return f"Error: Unexpected error executing command: {type(e).__name__}: {e}"

View File

@@ -7,6 +7,7 @@ from src.sandbox.tools import (
mask_local_paths_in_output,
replace_virtual_path,
resolve_local_tool_path,
validate_local_bash_command_paths,
)
@@ -56,3 +57,27 @@ def test_resolve_local_tool_path_rejects_path_traversal() -> None:
with pytest.raises(PermissionError, match="path traversal"):
resolve_local_tool_path(f"{VIRTUAL_PATH_PREFIX}/workspace/../../../../etc/passwd", thread_data)
def test_validate_local_bash_command_paths_blocks_host_paths() -> None:
thread_data = {
"workspace_path": "/tmp/deer-flow/threads/t1/user-data/workspace",
"uploads_path": "/tmp/deer-flow/threads/t1/user-data/uploads",
"outputs_path": "/tmp/deer-flow/threads/t1/user-data/outputs",
}
with pytest.raises(PermissionError, match="Unsafe absolute paths"):
validate_local_bash_command_paths("cat /etc/passwd", thread_data)
def test_validate_local_bash_command_paths_allows_virtual_and_system_paths() -> None:
thread_data = {
"workspace_path": "/tmp/deer-flow/threads/t1/user-data/workspace",
"uploads_path": "/tmp/deer-flow/threads/t1/user-data/uploads",
"outputs_path": "/tmp/deer-flow/threads/t1/user-data/outputs",
}
validate_local_bash_command_paths(
"/bin/echo ok > /mnt/user-data/workspace/out.txt && cat /dev/null",
thread_data,
)