fix(sandbox): block host bash traversal escapes (#2560)

* fix(sandbox): block host bash traversal escapes

Fixes #2535

* fix(sandbox): harden local bash path guards

* fix(sandbox): avoid bash cd argument false positives

* Fix the lint error

Add function to resolve and validate user data path.

* Fix the lint error

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
DanielWalnut
2026-04-28 12:18:41 +08:00
committed by GitHub
parent 39c5da94f3
commit 6bd88fe14c
2 changed files with 373 additions and 25 deletions
@@ -346,6 +346,104 @@ def test_validate_local_bash_command_paths_blocks_traversal_in_skills() -> None:
)
@pytest.mark.parametrize(
"command",
[
"cat ../uploads/secret.txt",
"cat subdir/../../secret.txt",
"python script.py --input=../secret.txt",
"echo ok > ../outputs/result.txt",
],
)
def test_validate_local_bash_command_paths_blocks_relative_dotdot_segments(command: str) -> None:
with pytest.raises(PermissionError, match="path traversal"):
validate_local_bash_command_paths(command, _THREAD_DATA)
def test_validate_local_bash_command_paths_blocks_cd_root_escape() -> None:
with pytest.raises(PermissionError, match="Unsafe working directory"):
validate_local_bash_command_paths("cd / && cat etc/passwd", _THREAD_DATA)
def test_validate_local_bash_command_paths_blocks_cd_parent_escape() -> None:
with pytest.raises(PermissionError, match="path traversal"):
validate_local_bash_command_paths("cd .. && cat etc/passwd", _THREAD_DATA)
def test_validate_local_bash_command_paths_blocks_cd_env_var_escape() -> None:
with pytest.raises(PermissionError, match="Unsafe working directory"):
validate_local_bash_command_paths("cd $HOME && cat .ssh/id_rsa", _THREAD_DATA)
def test_validate_local_bash_command_paths_blocks_multiline_cd_escape() -> None:
with pytest.raises(PermissionError, match="Unsafe working directory"):
validate_local_bash_command_paths("echo ok\ncd $HOME && cat .ssh/id_rsa", _THREAD_DATA)
@pytest.mark.parametrize(
"command",
[
"command cd / && cat etc/passwd",
"builtin cd $HOME && cat .ssh/id_rsa",
"if cd $HOME; then cat .ssh/id_rsa; fi",
"{ cd /; cat etc/passwd; }",
'echo "$(cd $HOME && cat .ssh/id_rsa)"',
],
)
def test_validate_local_bash_command_paths_blocks_complex_cd_escapes(command: str) -> None:
with pytest.raises(PermissionError, match="Unsafe working directory"):
validate_local_bash_command_paths(command, _THREAD_DATA)
@pytest.mark.parametrize(
"command",
[
"ls /",
"ln -s / root && cat root/etc/passwd",
"command ls /",
],
)
def test_validate_local_bash_command_paths_blocks_bare_root_path(command: str) -> None:
with pytest.raises(PermissionError, match="Unsafe absolute paths"):
validate_local_bash_command_paths(command, _THREAD_DATA)
@pytest.mark.parametrize(
"command",
[
"echo cd /",
"printf '%s\\n' pushd /",
],
)
def test_validate_local_bash_command_paths_allows_cd_words_as_arguments(command: str) -> None:
validate_local_bash_command_paths(command, _THREAD_DATA)
def test_validate_local_bash_command_paths_allows_workspace_relative_paths() -> None:
validate_local_bash_command_paths(
"mkdir -p reports && python script.py data/input.csv > reports/out.txt",
_THREAD_DATA,
)
def test_validate_local_bash_command_paths_allows_cd_virtual_workspace_with_relative_paths() -> None:
validate_local_bash_command_paths(
"cd /mnt/user-data/workspace && cat data/input.csv > reports/out.txt",
_THREAD_DATA,
)
def test_validate_local_bash_command_paths_allows_http_url_dotdot_segments() -> None:
validate_local_bash_command_paths(
"curl https://example.com/packages/../archive.tar.gz -o /mnt/user-data/workspace/archive.tar.gz",
_THREAD_DATA,
)
validate_local_bash_command_paths(
"curl http://example.com/packages/../archive.tar.gz -o /mnt/user-data/workspace/archive.tar.gz",
_THREAD_DATA,
)
def test_bash_tool_rejects_host_bash_when_local_sandbox_default(monkeypatch) -> None:
runtime = SimpleNamespace(
state={"sandbox": {"sandbox_id": "local"}, "thread_data": _THREAD_DATA.copy()},
@@ -367,6 +465,28 @@ def test_bash_tool_rejects_host_bash_when_local_sandbox_default(monkeypatch) ->
assert "Host bash execution is disabled" in result
def test_bash_tool_blocks_relative_traversal_before_host_execution(monkeypatch) -> None:
runtime = SimpleNamespace(
state={"sandbox": {"sandbox_id": "local"}, "thread_data": _THREAD_DATA.copy()},
context={"thread_id": "thread-1"},
)
monkeypatch.setattr(
"deerflow.sandbox.tools.ensure_sandbox_initialized",
lambda runtime: SimpleNamespace(execute_command=lambda command: pytest.fail("unsafe command should not execute")),
)
monkeypatch.setattr("deerflow.sandbox.tools.ensure_thread_directories_exist", lambda runtime: None)
monkeypatch.setattr("deerflow.sandbox.tools.is_host_bash_allowed", lambda: True)
result = bash_tool.func(
runtime=runtime,
description="run command",
command="cat ../uploads/secret.txt",
)
assert "path traversal" in result
# ---------- Skills path tests ----------