fix(shell): reject LLM-supplied working_dir outside workspace (#2826)
This commit is contained in:
@@ -113,3 +113,71 @@ def test_exec_allows_reads_of_history_jsonl(command):
|
||||
tool = ExecTool()
|
||||
result = tool._guard_command(command, "/tmp")
|
||||
assert result is None
|
||||
|
||||
|
||||
# --- #2826: working_dir must not escape the configured workspace ---------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exec_blocks_working_dir_outside_workspace(tmp_path):
|
||||
"""An LLM-supplied working_dir outside the workspace must be rejected."""
|
||||
workspace = tmp_path / "workspace"
|
||||
workspace.mkdir()
|
||||
tool = ExecTool(working_dir=str(workspace), restrict_to_workspace=True)
|
||||
result = await tool.execute(command="rm calendar.ics", working_dir="/etc")
|
||||
assert "outside the configured workspace" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exec_blocks_absolute_rm_via_hijacked_working_dir(tmp_path):
|
||||
"""Regression for #2826: `rm /abs/path` via working_dir hijack."""
|
||||
workspace = tmp_path / "workspace"
|
||||
workspace.mkdir()
|
||||
victim_dir = tmp_path / "outside"
|
||||
victim_dir.mkdir()
|
||||
victim = victim_dir / "file.ics"
|
||||
victim.write_text("data")
|
||||
|
||||
tool = ExecTool(working_dir=str(workspace), restrict_to_workspace=True)
|
||||
result = await tool.execute(
|
||||
command=f"rm {victim}",
|
||||
working_dir=str(victim_dir),
|
||||
)
|
||||
assert "outside the configured workspace" in result
|
||||
assert victim.exists(), "victim file must not have been deleted"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exec_allows_working_dir_within_workspace(tmp_path):
|
||||
"""A working_dir that is a subdirectory of the workspace is fine."""
|
||||
workspace = tmp_path / "workspace"
|
||||
subdir = workspace / "project"
|
||||
subdir.mkdir(parents=True)
|
||||
tool = ExecTool(working_dir=str(workspace), restrict_to_workspace=True, timeout=5)
|
||||
result = await tool.execute(command="echo ok", working_dir=str(subdir))
|
||||
assert "ok" in result
|
||||
assert "outside the configured workspace" not in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exec_allows_working_dir_equal_to_workspace(tmp_path):
|
||||
"""Passing working_dir equal to the workspace root must be allowed."""
|
||||
workspace = tmp_path / "workspace"
|
||||
workspace.mkdir()
|
||||
tool = ExecTool(working_dir=str(workspace), restrict_to_workspace=True, timeout=5)
|
||||
result = await tool.execute(command="echo ok", working_dir=str(workspace))
|
||||
assert "ok" in result
|
||||
assert "outside the configured workspace" not in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exec_ignores_workspace_check_when_not_restricted(tmp_path):
|
||||
"""Without restrict_to_workspace, the LLM may still choose any working_dir."""
|
||||
workspace = tmp_path / "workspace"
|
||||
workspace.mkdir()
|
||||
other = tmp_path / "other"
|
||||
other.mkdir()
|
||||
tool = ExecTool(working_dir=str(workspace), restrict_to_workspace=False, timeout=5)
|
||||
result = await tool.execute(command="echo ok", working_dir=str(other))
|
||||
assert "ok" in result
|
||||
assert "outside the configured workspace" not in result
|
||||
|
||||
Reference in New Issue
Block a user