mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-20 23:21:06 +00:00
[Security] Address critical host-shell escape in LocalSandboxProvider (#1547)
* fix(security): disable host bash by default in local sandbox * fix(security): address review feedback for local bash hardening * fix(ci): sort live test imports for lint * style: apply backend formatter --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
@@ -72,7 +72,7 @@ def _make_e2e_config() -> AppConfig:
|
||||
supports_vision=False,
|
||||
)
|
||||
],
|
||||
sandbox=SandboxConfig(use="deerflow.sandbox.local:LocalSandboxProvider"),
|
||||
sandbox=SandboxConfig(use="deerflow.sandbox.local:LocalSandboxProvider", allow_host_bash=True),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
from deerflow.client import DeerFlowClient, StreamEvent
|
||||
from deerflow.sandbox.security import is_host_bash_allowed
|
||||
from deerflow.uploads.manager import PathTraversalError
|
||||
|
||||
# Skip entire module in CI or when no config.yaml exists
|
||||
@@ -100,6 +101,9 @@ class TestLiveStreaming:
|
||||
class TestLiveToolUse:
|
||||
def test_agent_uses_bash_tool(self, client):
|
||||
"""Agent uses bash tool when asked to run a command."""
|
||||
if not is_host_bash_allowed():
|
||||
pytest.skip("Host bash is disabled for LocalSandboxProvider in the active config")
|
||||
|
||||
events = list(client.stream("Use the bash tool to run: echo 'LIVE_TEST_OK'. Then tell me the output."))
|
||||
|
||||
types = [e.type for e in events]
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
from deerflow.tools.tools import get_available_tools
|
||||
|
||||
|
||||
def _make_config(*, allow_host_bash: bool, sandbox_use: str = "deerflow.sandbox.local:LocalSandboxProvider", extra_tools: list[SimpleNamespace] | None = None):
|
||||
return SimpleNamespace(
|
||||
tools=[
|
||||
SimpleNamespace(name="bash", group="bash", use="deerflow.sandbox.tools:bash_tool"),
|
||||
SimpleNamespace(name="ls", group="file:read", use="tests:ls_tool"),
|
||||
*(extra_tools or []),
|
||||
],
|
||||
models=[],
|
||||
sandbox=SimpleNamespace(
|
||||
use=sandbox_use,
|
||||
allow_host_bash=allow_host_bash,
|
||||
),
|
||||
tool_search=SimpleNamespace(enabled=False),
|
||||
get_model_config=lambda name: None,
|
||||
)
|
||||
|
||||
|
||||
def test_get_available_tools_hides_bash_for_default_local_sandbox(monkeypatch):
|
||||
monkeypatch.setattr("deerflow.tools.tools.get_app_config", lambda: _make_config(allow_host_bash=False))
|
||||
monkeypatch.setattr(
|
||||
"deerflow.tools.tools.resolve_variable",
|
||||
lambda use, _: SimpleNamespace(name="bash" if "bash" in use else "ls"),
|
||||
)
|
||||
|
||||
names = [tool.name for tool in get_available_tools(include_mcp=False, subagent_enabled=False)]
|
||||
|
||||
assert "bash" not in names
|
||||
assert "ls" in names
|
||||
|
||||
|
||||
def test_get_available_tools_keeps_bash_when_explicitly_enabled(monkeypatch):
|
||||
monkeypatch.setattr("deerflow.tools.tools.get_app_config", lambda: _make_config(allow_host_bash=True))
|
||||
monkeypatch.setattr(
|
||||
"deerflow.tools.tools.resolve_variable",
|
||||
lambda use, _: SimpleNamespace(name="bash" if "bash" in use else "ls"),
|
||||
)
|
||||
|
||||
names = [tool.name for tool in get_available_tools(include_mcp=False, subagent_enabled=False)]
|
||||
|
||||
assert "bash" in names
|
||||
assert "ls" in names
|
||||
|
||||
|
||||
def test_get_available_tools_hides_renamed_host_bash_alias(monkeypatch):
|
||||
config = _make_config(
|
||||
allow_host_bash=False,
|
||||
extra_tools=[SimpleNamespace(name="shell", group="bash", use="deerflow.sandbox.tools:bash_tool")],
|
||||
)
|
||||
monkeypatch.setattr("deerflow.tools.tools.get_app_config", lambda: config)
|
||||
monkeypatch.setattr(
|
||||
"deerflow.tools.tools.resolve_variable",
|
||||
lambda use, _: SimpleNamespace(name="bash" if "bash_tool" in use else "ls"),
|
||||
)
|
||||
|
||||
names = [tool.name for tool in get_available_tools(include_mcp=False, subagent_enabled=False)]
|
||||
|
||||
assert "bash" not in names
|
||||
assert "shell" not in names
|
||||
assert "ls" in names
|
||||
|
||||
|
||||
def test_get_available_tools_keeps_bash_for_aio_sandbox(monkeypatch):
|
||||
config = _make_config(
|
||||
allow_host_bash=False,
|
||||
sandbox_use="deerflow.community.aio_sandbox:AioSandboxProvider",
|
||||
)
|
||||
monkeypatch.setattr("deerflow.tools.tools.get_app_config", lambda: config)
|
||||
monkeypatch.setattr(
|
||||
"deerflow.tools.tools.resolve_variable",
|
||||
lambda use, _: SimpleNamespace(name="bash" if "bash_tool" in use else "ls"),
|
||||
)
|
||||
|
||||
names = [tool.name for tool in get_available_tools(include_mcp=False, subagent_enabled=False)]
|
||||
|
||||
assert "bash" in names
|
||||
assert "ls" in names
|
||||
@@ -1,4 +1,5 @@
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
@@ -11,6 +12,7 @@ from deerflow.sandbox.tools import (
|
||||
_resolve_acp_workspace_path,
|
||||
_resolve_and_validate_user_data_path,
|
||||
_resolve_skills_path,
|
||||
bash_tool,
|
||||
mask_local_paths_in_output,
|
||||
replace_virtual_path,
|
||||
replace_virtual_paths_in_command,
|
||||
@@ -255,6 +257,27 @@ def test_validate_local_bash_command_paths_blocks_traversal_in_skills() -> None:
|
||||
)
|
||||
|
||||
|
||||
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()},
|
||||
context={"thread_id": "thread-1"},
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"deerflow.sandbox.tools.ensure_sandbox_initialized",
|
||||
lambda runtime: SimpleNamespace(execute_command=lambda command: pytest.fail("host bash should not execute")),
|
||||
)
|
||||
monkeypatch.setattr("deerflow.sandbox.tools.is_host_bash_allowed", lambda: False)
|
||||
|
||||
result = bash_tool.func(
|
||||
runtime=runtime,
|
||||
description="run command",
|
||||
command="/bin/echo hello",
|
||||
)
|
||||
|
||||
assert "Host bash execution is disabled" in result
|
||||
|
||||
|
||||
# ---------- Skills path tests ----------
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
"""Tests for subagent availability and prompt exposure under local bash hardening."""
|
||||
|
||||
from deerflow.agents.lead_agent import prompt as prompt_module
|
||||
from deerflow.subagents import registry as registry_module
|
||||
|
||||
|
||||
def test_get_available_subagent_names_hides_bash_when_host_bash_disabled(monkeypatch) -> None:
|
||||
monkeypatch.setattr(registry_module, "is_host_bash_allowed", lambda: False)
|
||||
|
||||
names = registry_module.get_available_subagent_names()
|
||||
|
||||
assert names == ["general-purpose"]
|
||||
|
||||
|
||||
def test_get_available_subagent_names_keeps_bash_when_allowed(monkeypatch) -> None:
|
||||
monkeypatch.setattr(registry_module, "is_host_bash_allowed", lambda: True)
|
||||
|
||||
names = registry_module.get_available_subagent_names()
|
||||
|
||||
assert names == ["general-purpose", "bash"]
|
||||
|
||||
|
||||
def test_build_subagent_section_hides_bash_examples_when_unavailable(monkeypatch) -> None:
|
||||
monkeypatch.setattr(prompt_module, "get_available_subagent_names", lambda: ["general-purpose"])
|
||||
|
||||
section = prompt_module._build_subagent_section(3)
|
||||
|
||||
assert "Not available in the current sandbox configuration" in section
|
||||
assert 'bash("npm test")' not in section
|
||||
assert 'read_file("/mnt/user-data/workspace/README.md")' in section
|
||||
assert "available tools (ls, read_file, web_search, etc.)" in section
|
||||
|
||||
|
||||
def test_build_subagent_section_includes_bash_when_available(monkeypatch) -> None:
|
||||
monkeypatch.setattr(prompt_module, "get_available_subagent_names", lambda: ["general-purpose", "bash"])
|
||||
|
||||
section = prompt_module._build_subagent_section(3)
|
||||
|
||||
assert "For command execution (git, build, test, deploy operations)" in section
|
||||
assert 'bash("npm test")' in section
|
||||
assert "available tools (bash, ls, read_file, web_search, etc.)" in section
|
||||
@@ -64,8 +64,12 @@ def _make_result(
|
||||
)
|
||||
|
||||
|
||||
def _run_task_tool(**kwargs):
|
||||
return asyncio.run(task_tool_module.task_tool.coroutine(**kwargs))
|
||||
def _run_task_tool(**kwargs) -> str:
|
||||
"""Execute the task tool across LangChain sync/async wrapper variants."""
|
||||
coroutine = getattr(task_tool_module.task_tool, "coroutine", None)
|
||||
if coroutine is not None:
|
||||
return asyncio.run(coroutine(**kwargs))
|
||||
return task_tool_module.task_tool.func(**kwargs)
|
||||
|
||||
|
||||
async def _no_sleep(_: float) -> None:
|
||||
@@ -79,6 +83,7 @@ class _DummyScheduledTask:
|
||||
|
||||
def test_task_tool_returns_error_for_unknown_subagent(monkeypatch):
|
||||
monkeypatch.setattr(task_tool_module, "get_subagent_config", lambda _: None)
|
||||
monkeypatch.setattr(task_tool_module, "get_available_subagent_names", lambda: ["general-purpose"])
|
||||
|
||||
result = _run_task_tool(
|
||||
runtime=None,
|
||||
@@ -88,7 +93,22 @@ def test_task_tool_returns_error_for_unknown_subagent(monkeypatch):
|
||||
tool_call_id="tc-1",
|
||||
)
|
||||
|
||||
assert result.startswith("Error: Unknown subagent type")
|
||||
assert result == "Error: Unknown subagent type 'general-purpose'. Available: general-purpose"
|
||||
|
||||
|
||||
def test_task_tool_rejects_bash_subagent_when_host_bash_disabled(monkeypatch):
|
||||
monkeypatch.setattr(task_tool_module, "get_subagent_config", lambda _: _make_subagent_config())
|
||||
monkeypatch.setattr(task_tool_module, "is_host_bash_allowed", lambda: False)
|
||||
|
||||
result = _run_task_tool(
|
||||
runtime=_make_runtime(),
|
||||
description="执行任务",
|
||||
prompt="run commands",
|
||||
subagent_type="bash",
|
||||
tool_call_id="tc-bash",
|
||||
)
|
||||
|
||||
assert result.startswith("Error: Bash subagent is disabled")
|
||||
|
||||
|
||||
def test_task_tool_emits_running_and_completed_events(monkeypatch):
|
||||
|
||||
Reference in New Issue
Block a user