fix: replace eval() with safe AST parsing to prevent code injection

- Replace dangerous eval() usage in calculator tools with safe AST-based mathematical expression parser
- Add input validation with regex to prevent injection of non-mathematical content
- Implement proper error handling for malformed expressions
- Update ExaSearchTool to use JSON parsing + ast.literal_eval() instead of eval()
- Maintain backward compatibility while eliminating critical security vulnerability

Addresses CVE-level code injection risk that could lead to arbitrary code execution
This commit is contained in:
theCyberTech
2025-06-24 16:11:35 +08:00
parent 3dd39ad638
commit b0fdfefdb8
3 changed files with 136 additions and 12 deletions

View File

@@ -1,4 +1,6 @@
import os
import ast
import json
from exa_py import Exa
from langchain.agents import tool
@@ -20,12 +22,34 @@ class ExaSearchTool:
"""Get the contents of a webpage.
The ids must be passed in as a list, a list of ids returned from `search`.
"""
ids = eval(ids)
contents = str(ExaSearchTool._exa().get_contents(ids))
print(contents)
contents = contents.split("URL:")
contents = [content[:1000] for content in contents]
return "\n\n".join(contents)
try:
# Try to parse as JSON first (safer)
try:
ids = json.loads(ids)
except (json.JSONDecodeError, TypeError):
# If JSON parsing fails, try ast.literal_eval as a fallback
try:
ids = ast.literal_eval(ids)
except (ValueError, SyntaxError):
# If both fail, assume it's a single ID string
ids = [ids.strip()] if isinstance(ids, str) else []
# Validate that ids is a list
if not isinstance(ids, list):
return "Error: IDs must be provided as a list"
# Validate each ID is a string
if not all(isinstance(id_val, str) for id_val in ids):
return "Error: All IDs must be strings"
contents = str(ExaSearchTool._exa().get_contents(ids))
print(contents)
contents = contents.split("URL:")
contents = [content[:1000] for content in contents]
return "\n\n".join(contents)
except Exception as e:
return f"Error processing IDs: {str(e)}"
def tools():
return [ExaSearchTool.search, ExaSearchTool.find_similar, ExaSearchTool.get_contents]

View File

@@ -1,4 +1,7 @@
from crewai_tools import BaseTool
import ast
import operator
import re
class CalculatorTool(BaseTool):
@@ -7,6 +10,54 @@ class CalculatorTool(BaseTool):
"Useful to perform any mathematical calculations, like sum, minus, multiplication, division, etc. The input to this tool should be a mathematical expression, a couple examples are `200*7` or `5000/2*10."
)
def _run(self, operation: str) -> int:
# Implementation goes here
return eval(operation)
def _run(self, operation: str) -> float:
try:
# Define allowed operators for safe evaluation
allowed_operators = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.Pow: operator.pow,
ast.Mod: operator.mod,
ast.USub: operator.neg,
ast.UAdd: operator.pos,
}
# Parse and validate the expression
if not re.match(r'^[0-9+\-*/().% ]+$', operation):
raise ValueError("Invalid characters in mathematical expression")
# Parse the expression
tree = ast.parse(operation, mode='eval')
def _eval_node(node):
if isinstance(node, ast.Expression):
return _eval_node(node.body)
elif isinstance(node, ast.Constant): # Python 3.8+
return node.value
elif isinstance(node, ast.Num): # Python < 3.8
return node.n
elif isinstance(node, ast.BinOp):
left = _eval_node(node.left)
right = _eval_node(node.right)
op = allowed_operators.get(type(node.op))
if op is None:
raise ValueError(f"Unsupported operator: {type(node.op).__name__}")
return op(left, right)
elif isinstance(node, ast.UnaryOp):
operand = _eval_node(node.operand)
op = allowed_operators.get(type(node.op))
if op is None:
raise ValueError(f"Unsupported operator: {type(node.op).__name__}")
return op(operand)
else:
raise ValueError(f"Unsupported node type: {type(node).__name__}")
result = _eval_node(tree)
return result
except (SyntaxError, ValueError, ZeroDivisionError, TypeError) as e:
raise ValueError(f"Calculation error: {str(e)}")
except Exception:
raise ValueError("Invalid mathematical expression")

View File

@@ -1,4 +1,7 @@
from langchain.tools import tool
import ast
import operator
import re
class CalculatorTools():
@@ -10,6 +13,52 @@ class CalculatorTools():
expression, a couple examples are `200*7` or `5000/2*10`
"""
try:
return eval(operation)
except SyntaxError:
return "Error: Invalid syntax in mathematical expression"
# Define allowed operators for safe evaluation
allowed_operators = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.Pow: operator.pow,
ast.Mod: operator.mod,
ast.USub: operator.neg,
ast.UAdd: operator.pos,
}
# Parse and validate the expression
if not re.match(r'^[0-9+\-*/().% ]+$', operation):
return "Error: Invalid characters in mathematical expression"
# Parse the expression
tree = ast.parse(operation, mode='eval')
def _eval_node(node):
if isinstance(node, ast.Expression):
return _eval_node(node.body)
elif isinstance(node, ast.Constant): # Python 3.8+
return node.value
elif isinstance(node, ast.Num): # Python < 3.8
return node.n
elif isinstance(node, ast.BinOp):
left = _eval_node(node.left)
right = _eval_node(node.right)
op = allowed_operators.get(type(node.op))
if op is None:
raise ValueError(f"Unsupported operator: {type(node.op).__name__}")
return op(left, right)
elif isinstance(node, ast.UnaryOp):
operand = _eval_node(node.operand)
op = allowed_operators.get(type(node.op))
if op is None:
raise ValueError(f"Unsupported operator: {type(node.op).__name__}")
return op(operand)
else:
raise ValueError(f"Unsupported node type: {type(node).__name__}")
result = _eval_node(tree)
return result
except (SyntaxError, ValueError, ZeroDivisionError, TypeError) as e:
return f"Error: {str(e)}"
except Exception as e:
return "Error: Invalid mathematical expression"