Root cause
Injection & unsafe execution introduced
fix: secure python_tools.py exec() with subprocess sandbox (#1199)
How AI contributed: Direct introductionPublished Apr 8, 2026 in
mervinpraison/praisonaiAll 19 findings in this repository
AI-generated sandbox code for execute_code wrote a subprocess wrapper whose inline blocked_attrs list held only eleven names while the direct-mode list held more than thirty. The frame-traversal chain from __traceback__ through tb_frame and f_back to f_builtins was missing from the subprocess list, so submitted code could reach the real builtins and run arbitrary commands in the subprocess, bypassing the AST blocklist and the check that looks for exec by name. Version 1.5.115 aligned the subprocess list with the full direct-mode list.
_execute_code_sandboxed writes a subprocess wrapper whose inline blocked_attrs omits __traceback__, tb_frame and f_builtins, so an exception object exposes the caller's frame and the real builtins behind it. The wrapper also screens only attribute names that appear literally in the submitted source, so the same access works when the name is reached through an alias.
Root cause
fix: secure python_tools.py exec() with subprocess sandbox (#1199)
Fix
feat(context): enhance agent ledger and monitor functionality
Fix by MervinPraison · no AI marker found
Case facts
@@ -15,7 +15,13 @@ import io from contextlib import redirect_stdout, redirect_stderr import traceback+import subprocess+import tempfile+import os+import json+import sys from ..approval import require_approval+from ..sandbox.protocols import ResourceLimits def _safe_getattr(obj, name, *default): """getattr wrapper that blocks access to dunder attributes."""@@ -78,25 +84,259 @@ def _validate_code_ast(code: str): return None +def _execute_code_sandboxed(+ code: str,+ timeout: int = 30,+ max_output_size: int = 10000,+ limits: Optional[ResourceLimits] = None+) -> Dict[str, Any]:+ """Execute Python code in a subprocess sandbox.+ + This function runs code in a separate Python subprocess with resource limits+ and restricted environment, providing better isolation than direct exec().+ """+ if limits is None:+ limits = ResourceLimits.minimal()+ + try:+ # Create temporary file for the code+ with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:+ # Create the wrapper code with proper escaping+ # Use repr() to safely embed the user code as a string literal+ wrapper_code = f"""+import sys+import io+import traceback+import json++def safe_execute():+ # Execute user code safely and return results as JSON+ stdout_buffer = io.StringIO()+ stderr_buffer = io.StringIO()+ result = None+ + try:+ # Redirect output+ sys.stdout = stdout_buffer+ sys.stderr = stderr_buffer+ + # Get user code+ user_code = {repr(code)}+ + # Security validation (same AST checks as direct execution)+ import ast+ try:+ tree = ast.parse(user_code)+ except SyntaxError:+ return {{+ "result": None,+ "stdout": "",+ "stderr": "Syntax Error in provided code",+ "success": False+ }}+ + # Block dangerous patterns+ blocked_attrs = {{+ '__subclasses__', '__bases__', '__mro__', '__globals__',+ '__code__', '__class__', '__dict__', '__builtins__',+ '__import__', '__loader__', '__spec__'+ }}+ + for node in ast.walk(tree):+ if isinstance(node, (ast.Import, ast.ImportFrom)):+ return {{+ "result": None,+ "stdout": "",+ "stderr": "Import statements are not allowed in sandbox mode",+ "success": False+ }}+ if isinstance(node, ast.Attribute) and node.attr in blocked_attrs:+ return {{+ "result": None,+ "stdout": "",+ "stderr": f"Access to attribute '{{node.attr}}' is restricted",+ "success": False@@ -702,6 +702,10 @@ class ExecutionConfig: code_execution: bool = False code_mode: str = "safe" # "safe" or "unsafe" + # Code execution sandbox mode - "sandbox" (default) uses subprocess isolation+ # "direct" runs in current process (legacy, less secure)+ code_sandbox_mode: str = "sandbox" # "sandbox" or "direct"+ # Rate limiter instance (consolidated from standalone rate_limiter param) rate_limiter: Optional[Any] = None @@ -730,6 +734,7 @@ def to_dict(self) -> Dict[str, Any]: "max_retry_limit": self.max_retry_limit, "code_execution": self.code_execution, "code_mode": self.code_mode,+ "code_sandbox_mode": self.code_sandbox_mode, "context_compaction": self.context_compaction, "max_context_tokens": self.max_context_tokens, "max_budget": self.max_budget,@@ -143,7 +143,15 @@ def safe_execute(): blocked_attrs = {{ '__subclasses__', '__bases__', '__mro__', '__globals__', '__code__', '__class__', '__dict__', '__builtins__',- '__import__', '__loader__', '__spec__'+ '__import__', '__loader__', '__spec__', '__init_subclass__',+ '__set_name__', '__reduce__', '__reduce_ex__',+ '__traceback__', '__qualname__', '__module__',+ '__wrapped__', '__closure__', '__annotations__',+ # Frame/code object introspection+ 'gi_frame', 'gi_code', 'cr_frame', 'cr_code',+ 'ag_frame', 'ag_code', 'tb_frame', 'tb_next',+ 'f_globals', 'f_locals', 'f_builtins', 'f_code',+ 'co_consts', 'co_names', }} for node in ast.walk(tree):@@ -162,7 +170,9 @@ def safe_execute(): "success": False }} if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):- if node.func.id in ('exec', 'eval', 'compile', '__import__', 'open'):+ if node.func.id in ('exec', 'eval', 'compile', '__import__',+ 'open', 'input', 'breakpoint',+ 'setattr', 'delattr', 'dir'): return {{ "result": None, "stdout": "",AI-assisted change 8bf1c93851f530385e6fd69c8c353f6507ae18f811d6e185ba90ed0c81b54b7d · Fix 371d188d3cf713f4e08b3188e801a0819872870c8bd5a33963c8f59d04c5ccef