mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
- Replace `--ro-bind / /` with whitelist-only filesystem: only /usr, /etc, /bin, /lib, /sbin mounted read-only. /app, /root, /home, /opt, /var are completely invisible inside the sandbox. - Add `--clearenv` to wipe all inherited env vars (API keys, DB passwords). Only safe vars (PATH, HOME=workspace, LANG) are explicitly set. - Remove python_exec tool — bash_exec can run `python3 -c` or heredocs with identical bubblewrap protection, reducing attack surface. - Remove all fallback security code (import hooks, blocked modules, network command lists). Tools now hard-require bubblewrap — disabled on platforms without bwrap. - Clean up security_hooks.py: remove ~200 lines of dead bash validation code, add Bash to BLOCKED_TOOLS as defence-in-depth. - Wire up long-running tool callback in SDK service for create_agent/edit_agent delegation to Redis Streams background infrastructure.
134 lines
4.7 KiB
Python
134 lines
4.7 KiB
Python
"""Tests for SDK security hooks — workspace paths, tool access, and deny messages.
|
|
|
|
These are pure unit tests with no external dependencies (no SDK, no DB, no server).
|
|
They validate that the security hooks correctly block unauthorized paths,
|
|
tool access, and dangerous input patterns.
|
|
|
|
Note: Bash command validation was removed — the SDK built-in Bash tool is not in
|
|
allowed_tools, and the bash_exec MCP tool has kernel-level network isolation
|
|
(unshare --net) making command-level parsing unnecessary.
|
|
"""
|
|
|
|
from backend.api.features.chat.sdk.security_hooks import (
|
|
_validate_tool_access,
|
|
_validate_workspace_path,
|
|
)
|
|
|
|
SDK_CWD = "/tmp/copilot-test-session"
|
|
|
|
|
|
def _is_denied(result: dict) -> bool:
|
|
hook = result.get("hookSpecificOutput", {})
|
|
return hook.get("permissionDecision") == "deny"
|
|
|
|
|
|
def _reason(result: dict) -> str:
|
|
return result.get("hookSpecificOutput", {}).get("permissionDecisionReason", "")
|
|
|
|
|
|
# ============================================================
|
|
# Workspace path validation (Read, Write, Edit, etc.)
|
|
# ============================================================
|
|
|
|
|
|
class TestWorkspacePathValidation:
|
|
def test_path_in_workspace(self):
|
|
result = _validate_workspace_path(
|
|
"Read", {"file_path": f"{SDK_CWD}/file.txt"}, SDK_CWD
|
|
)
|
|
assert not _is_denied(result)
|
|
|
|
def test_path_outside_workspace(self):
|
|
result = _validate_workspace_path("Read", {"file_path": "/etc/passwd"}, SDK_CWD)
|
|
assert _is_denied(result)
|
|
|
|
def test_tool_results_allowed(self):
|
|
result = _validate_workspace_path(
|
|
"Read",
|
|
{"file_path": "~/.claude/projects/abc/tool-results/out.txt"},
|
|
SDK_CWD,
|
|
)
|
|
assert not _is_denied(result)
|
|
|
|
def test_claude_settings_blocked(self):
|
|
result = _validate_workspace_path(
|
|
"Read", {"file_path": "~/.claude/settings.json"}, SDK_CWD
|
|
)
|
|
assert _is_denied(result)
|
|
|
|
def test_claude_projects_without_tool_results(self):
|
|
result = _validate_workspace_path(
|
|
"Read", {"file_path": "~/.claude/projects/abc/credentials.json"}, SDK_CWD
|
|
)
|
|
assert _is_denied(result)
|
|
|
|
def test_no_path_allowed(self):
|
|
"""Glob/Grep without path defaults to cwd — should be allowed."""
|
|
result = _validate_workspace_path("Grep", {"pattern": "foo"}, SDK_CWD)
|
|
assert not _is_denied(result)
|
|
|
|
def test_path_traversal_with_dotdot(self):
|
|
result = _validate_workspace_path(
|
|
"Read", {"file_path": f"{SDK_CWD}/../../../etc/passwd"}, SDK_CWD
|
|
)
|
|
assert _is_denied(result)
|
|
|
|
|
|
# ============================================================
|
|
# Tool access validation
|
|
# ============================================================
|
|
|
|
|
|
class TestToolAccessValidation:
|
|
def test_blocked_tools(self):
|
|
for tool in ("bash", "shell", "exec", "terminal", "command"):
|
|
result = _validate_tool_access(tool, {})
|
|
assert _is_denied(result), f"Tool '{tool}' should be blocked"
|
|
|
|
def test_bash_builtin_blocked(self):
|
|
"""SDK built-in Bash (capital) is blocked as defence-in-depth."""
|
|
result = _validate_tool_access("Bash", {"command": "echo hello"}, SDK_CWD)
|
|
assert _is_denied(result)
|
|
assert "Bash" in _reason(result)
|
|
|
|
def test_workspace_tools_delegate(self):
|
|
result = _validate_tool_access(
|
|
"Read", {"file_path": f"{SDK_CWD}/file.txt"}, SDK_CWD
|
|
)
|
|
assert not _is_denied(result)
|
|
|
|
def test_dangerous_pattern_blocked(self):
|
|
result = _validate_tool_access("SomeUnknownTool", {"data": "sudo rm -rf /"})
|
|
assert _is_denied(result)
|
|
|
|
def test_safe_unknown_tool_allowed(self):
|
|
result = _validate_tool_access("SomeSafeTool", {"data": "hello world"})
|
|
assert not _is_denied(result)
|
|
|
|
|
|
# ============================================================
|
|
# Deny message quality (ntindle feedback)
|
|
# ============================================================
|
|
|
|
|
|
class TestDenyMessageClarity:
|
|
"""Deny messages must include [SECURITY] and 'cannot be bypassed'
|
|
so the model knows the restriction is enforced, not a suggestion."""
|
|
|
|
def test_blocked_tool_message(self):
|
|
reason = _reason(_validate_tool_access("bash", {}))
|
|
assert "[SECURITY]" in reason
|
|
assert "cannot be bypassed" in reason
|
|
|
|
def test_bash_builtin_blocked_message(self):
|
|
reason = _reason(_validate_tool_access("Bash", {"command": "echo hello"}))
|
|
assert "[SECURITY]" in reason
|
|
assert "cannot be bypassed" in reason
|
|
|
|
def test_workspace_path_message(self):
|
|
reason = _reason(
|
|
_validate_workspace_path("Read", {"file_path": "/etc/passwd"}, SDK_CWD)
|
|
)
|
|
assert "[SECURITY]" in reason
|
|
assert "cannot be bypassed" in reason
|