mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-19 12:24:46 +08:00
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:
@@ -14,6 +14,16 @@ from src.sandbox.exceptions import (
|
|||||||
from src.sandbox.sandbox import Sandbox
|
from src.sandbox.sandbox import Sandbox
|
||||||
from src.sandbox.sandbox_provider import get_sandbox_provider
|
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:
|
def replace_virtual_path(path: str, thread_data: ThreadDataState | None) -> str:
|
||||||
"""Replace virtual /mnt/user-data paths with actual thread data paths.
|
"""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")
|
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:
|
def replace_virtual_paths_in_command(command: str, thread_data: ThreadDataState | None) -> str:
|
||||||
"""Replace all virtual /mnt/user-data paths in a command string.
|
"""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)
|
ensure_thread_directories_exist(runtime)
|
||||||
thread_data = get_thread_data(runtime)
|
thread_data = get_thread_data(runtime)
|
||||||
if is_local_sandbox(runtime):
|
if is_local_sandbox(runtime):
|
||||||
|
validate_local_bash_command_paths(command, thread_data)
|
||||||
command = replace_virtual_paths_in_command(command, thread_data)
|
command = replace_virtual_paths_in_command(command, thread_data)
|
||||||
output = sandbox.execute_command(command)
|
output = sandbox.execute_command(command)
|
||||||
return mask_local_paths_in_output(output, thread_data)
|
return mask_local_paths_in_output(output, thread_data)
|
||||||
return sandbox.execute_command(command)
|
return sandbox.execute_command(command)
|
||||||
except SandboxError as e:
|
except SandboxError as e:
|
||||||
return f"Error: {e}"
|
return f"Error: {e}"
|
||||||
|
except PermissionError as e:
|
||||||
|
return f"Error: {e}"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error: Unexpected error executing command: {type(e).__name__}: {e}"
|
return f"Error: Unexpected error executing command: {type(e).__name__}: {e}"
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from src.sandbox.tools import (
|
|||||||
mask_local_paths_in_output,
|
mask_local_paths_in_output,
|
||||||
replace_virtual_path,
|
replace_virtual_path,
|
||||||
resolve_local_tool_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"):
|
with pytest.raises(PermissionError, match="path traversal"):
|
||||||
resolve_local_tool_path(f"{VIRTUAL_PATH_PREFIX}/workspace/../../../../etc/passwd", thread_data)
|
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,
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user