From 9f50ef7bc927cbc4034fd0b96dfdfe2394ffe4d5 Mon Sep 17 00:00:00 2001 From: FailSafe Researcher Date: Tue, 9 Jun 2026 20:22:30 -0700 Subject: [PATCH 1/3] fix: Restrict eval() in condition engine to prevent RCE The workflow condition evaluator used eval() with only a bare except to handle failures, allowing arbitrary Python code execution via attacker-controlled workflow expressions. Replace with a sandboxed eval(__builtins__={}) and an allowlist regex that rejects any expression containing dunder attributes, import, exec, open, or introspection builtins. Fixes: Code injection in agent_flow/engine/executor.py line 1203 Signed-off-by: FailSafe Researcher --- python_a2a/agent_flow/engine/executor.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/python_a2a/agent_flow/engine/executor.py b/python_a2a/agent_flow/engine/executor.py index 7d8b6f2..d478b8a 100644 --- a/python_a2a/agent_flow/engine/executor.py +++ b/python_a2a/agent_flow/engine/executor.py @@ -1198,9 +1198,12 @@ def _execute_conditional_node(self, node: WorkflowNode, execution: NodeExecution else: expr = expr.replace("$input", json.dumps(content)) - # Very basic evaluation (CAUTION: Not secure for production) - # A real implementation would use a proper JS engine or safe eval - result = bool(eval(expr)) + # Safe evaluation: only allow simple boolean/comparison expressions + # Reject any expression containing attribute access, calls, or imports + import re as _re + if _re.search(r'__|import|exec|open|getattr|setattr|globals|locals|vars|compile', expr): + raise ValueError(f"Unsafe expression rejected: {expr!r}") + result = bool(eval(expr, {"__builtins__": {}})) except: result = False From cba9c27277d24b79fb864f61f461d01f21615b34 Mon Sep 17 00:00:00 2001 From: FailSafe Researcher Date: Tue, 9 Jun 2026 20:22:30 -0700 Subject: [PATCH 2/3] fix: Replace blocklist eval with AST-whitelist safe evaluator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous fix used a keyword blocklist + eval(__builtins__={}) which is an incomplete sandbox — blocklists can be bypassed and the approach gives a false sense of security. Replace with an AST-based whitelist that only permits literals, arithmetic, comparison, and boolean operators (_SAFE_AST_NODES). Any other AST node type (calls, attribute access, imports, subscripts) is rejected before eval() is ever called. Rejection is logged with the offending node type. Also: - Collapse duplicate if/else branches into a single line - Accept 'javascript' as a deprecated alias for backwards compatibility - Remove import-inside-loop antipattern (ast imported at module level) Signed-off-by: FailSafe Researcher --- python_a2a/agent_flow/engine/executor.py | 61 ++++++++++++++++-------- 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/python_a2a/agent_flow/engine/executor.py b/python_a2a/agent_flow/engine/executor.py index d478b8a..80b2ed0 100644 --- a/python_a2a/agent_flow/engine/executor.py +++ b/python_a2a/agent_flow/engine/executor.py @@ -5,6 +5,7 @@ managing execution state, and handling message flow between nodes. """ +import ast import json import time import uuid @@ -13,6 +14,38 @@ from enum import Enum, auto from typing import Dict, List, Optional, Set, Any, Union, Tuple, Callable +# Whitelist of AST node types allowed in workflow condition expressions. +# Permits literals, arithmetic, comparisons, and boolean logic only. +# Any other node type (calls, attribute access, imports, etc.) is rejected. +_SAFE_AST_NODES = ( + ast.Expression, ast.BoolOp, ast.Compare, ast.BinOp, ast.UnaryOp, + ast.Constant, ast.And, ast.Or, ast.Not, + ast.Eq, ast.NotEq, ast.Lt, ast.LtE, ast.Gt, ast.GtE, + ast.In, ast.NotIn, ast.Add, ast.Sub, ast.Mult, ast.Div, + ast.Mod, ast.UAdd, ast.USub, ast.List, ast.Tuple, +) + + +def _safe_eval_expr(expr: str) -> bool: + """Evaluate a boolean/comparison expression using AST whitelisting. + + Only literals, arithmetic operators, and comparison/boolean operators + are permitted. Function calls, attribute access, imports, and builtins + are all rejected, so this is safe to run on untrusted input. + """ + try: + tree = ast.parse(expr, mode='eval') + except SyntaxError: + return False + for node in ast.walk(tree): + if not isinstance(node, _SAFE_AST_NODES): + logging.getLogger("WorkflowExecutor").warning( + "Condition expression rejected — unsafe AST node: %s in %r", + type(node).__name__, expr, + ) + return False + return bool(eval(compile(tree, '', 'eval'), {"__builtins__": {}})) + from ..models.workflow import ( Workflow, WorkflowNode, WorkflowEdge, NodeType, EdgeType ) @@ -1186,26 +1219,14 @@ def _execute_conditional_node(self, node: WorkflowNode, execution: NodeExecution content = input_message.content result = content == condition_value - elif condition_type == "javascript": - # Evaluate a JavaScript expression (simple implementation) - try: - # Replace placeholders in the expression - expr = condition_value - if input_message: - content = input_message.content - if isinstance(content, str): - expr = expr.replace("$input", json.dumps(content)) - else: - expr = expr.replace("$input", json.dumps(content)) - - # Safe evaluation: only allow simple boolean/comparison expressions - # Reject any expression containing attribute access, calls, or imports - import re as _re - if _re.search(r'__|import|exec|open|getattr|setattr|globals|locals|vars|compile', expr): - raise ValueError(f"Unsafe expression rejected: {expr!r}") - result = bool(eval(expr, {"__builtins__": {}})) - except: - result = False + elif condition_type in ("expression", "javascript"): + # "javascript" accepted as a deprecated alias for backwards compatibility. + # Evaluates a simple boolean/comparison expression via AST whitelisting — + # no function calls, attribute access, or builtins are permitted. + expr = condition_value + if input_message: + expr = expr.replace("$input", json.dumps(input_message.content)) + result = _safe_eval_expr(expr) # Create output message output_message = MessageValue( From 58bd0ddbe60ee10b6237687b74555b7a5dd40704 Mon Sep 17 00:00:00 2001 From: FailSafe Researcher Date: Tue, 9 Jun 2026 20:22:30 -0700 Subject: [PATCH 3/3] fix: add ast.Load to whitelist and catch eval() runtime errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs found during exploit testing: - ast.Load context node was missing from _SAFE_AST_NODES, causing list and tuple literals to be incorrectly rejected (false positive) - eval(compile(...)) was not wrapped in try/except, so arithmetic errors like ZeroDivisionError propagated up the call stack (DoS vector) Both are now fixed: ast.Load added (safe — ast.Name is still excluded, so variable name lookup is still blocked), and the eval call is wrapped in a broad except that returns False on any runtime error. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: FailSafe Researcher --- python_a2a/agent_flow/engine/executor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/python_a2a/agent_flow/engine/executor.py b/python_a2a/agent_flow/engine/executor.py index 80b2ed0..8f4798f 100644 --- a/python_a2a/agent_flow/engine/executor.py +++ b/python_a2a/agent_flow/engine/executor.py @@ -23,6 +23,7 @@ ast.Eq, ast.NotEq, ast.Lt, ast.LtE, ast.Gt, ast.GtE, ast.In, ast.NotIn, ast.Add, ast.Sub, ast.Mult, ast.Div, ast.Mod, ast.UAdd, ast.USub, ast.List, ast.Tuple, + ast.Load, # context marker for list/tuple literals — safe; ast.Name is still excluded ) @@ -44,7 +45,10 @@ def _safe_eval_expr(expr: str) -> bool: type(node).__name__, expr, ) return False - return bool(eval(compile(tree, '', 'eval'), {"__builtins__": {}})) + try: + return bool(eval(compile(tree, '', 'eval'), {"__builtins__": {}})) + except Exception: + return False from ..models.workflow import ( Workflow, WorkflowNode, WorkflowEdge, NodeType, EdgeType