AST 安全求值指的是:把表达式/代码先解析成抽象语法树(AST),然后不直接eval/compile执行,而是自己遍历 AST,只允许白名单内的节点,并按预定语义解释执行。核心目标是避免任意代码执行、沙箱逃逸和拒绝服务。
一句话:解析成 AST + 白名单解释器 + 资源限制 + 默认拒绝。
1. 为什么不直接用 eval?
Python 的eval、JS 的eval/new Function都极其危险:
python
eval("__import__('os').system('rm -rf /')")即使限制__builtins__,也可能通过属性链、类继承、生成器等方式逃逸。
所以安全求值不能依赖“过滤字符串”,而应基于 AST 白名单。
2. 安全求值的基本原则
默认拒绝:没显式允许的 AST 节点一律报错。
不调用、不导入、不访问属性:禁止
Call、Import、Attribute、Subscript等。变量来自受控环境:只允许简单类型,如
int/float/str/bool/None。运算符白名单:只允许
+ - * / // % **、比较、布尔等,并限制**。资源限制:输入长度、AST 节点数、递归深度、数值大小、字符串长度、执行时间。
纯函数、无副作用:求值不能修改外部状态。
3. Python 示例:安全表达式求值
只允许变量、常量、四则运算、比较、布尔、三元表达式。
python
import ast import operator as op _BIN = { ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul, ast.Div: op.truediv, ast.FloorDiv: op.floordiv, ast.Mod: op.mod, } _UNARY = { ast.UAdd: op.pos, ast.USub: op.neg, ast.Not: op.not_, } _CMP = { ast.Eq: op.eq, ast.NotEq: op.ne, ast.Lt: op.lt, ast.LtE: op.le, ast.Gt: op.gt, ast.GtE: op.ge, ast.In: lambda a, b: a in b, ast.NotIn: lambda a, b: a not in b, } _ALLOWED_TYPES = (int, float, str, bool, type(None)) def safe_eval(expr: str, variables=None, max_nodes=1000, max_pow=100): variables = variables or {} tree = ast.parse(expr, mode="eval") if sum(1 for _ in ast.walk(tree)) > max_nodes: raise ValueError("表达式过大") return _eval(tree, variables, max_pow) def _eval(node, env, max_pow): if isinstance(node, ast.Expression): return _eval(node.body, env, max_pow) if isinstance(node, ast.Constant): if isinstance(node.value, _ALLOWED_TYPES): return node.value raise ValueError("不允许的常量") if isinstance(node, ast.Name): if node.id in env and isinstance(env[node.id], _ALLOWED_TYPES): return env[node.id] raise ValueError(f"未知或不允许的变量: {node.id}") if isinstance(node, ast.BinOp): left = _eval(node.left, env, max_pow) right = _eval(node.right, env, max_pow) if isinstance(node.op, ast.Pow): if not isinstance(right, (int, float)) or abs(right) > max_pow: raise ValueError("指数过大") if isinstance(left, (int, float)) and abs(left) > 1e6: raise ValueError("底数过大") return left ** right fn = _BIN.get(type(node.op)) if fn is None: raise ValueError("不允许的二元运算") # 防止字符串乘法耗尽内存 if isinstance(node.op, ast.Mult): if isinstance(left, str) and isinstance(right, int) and len(left) * right > 10000: raise ValueError("字符串过长") if isinstance(right, str) and isinstance(left, int) and len(right) * left > 10000: raise ValueError("字符串过长") return fn(left, right) if isinstance(node, ast.UnaryOp): fn = _UNARY.get(type(node.op)) if fn is None: raise ValueError("不允许的一元运算") return fn(_eval(node.operand, env, max_pow)) if isinstance(node, ast.BoolOp): if isinstance(node.op, ast.And): result = True for v in node.values: result = _eval(v, env, max_pow) if not result: return result return result if isinstance(node.op, ast.Or): result = False for v in node.values: result = _eval(v, env, max_pow) if result: return result return result raise ValueError("不允许的布尔运算") if isinstance(node, ast.Compare): left = _eval(node.left, env, max_pow) for op_node, comparator in zip(node.ops, node.comparators): right = _eval(comparator, env, max_pow) fn = _CMP.get(type(op_node)) if fn is None or not fn(left, right): return False left = right return True if isinstance(node, ast.IfExp): cond = _eval(node.test, env, max_pow) return _eval(node.body if cond else node.orelse, env, max_pow) raise ValueError(f"不允许的语法: {type(node).__name__}")使用:
python
safe_eval("a + 2 * (b - 1)", {"a": 3, "b": 5}) # 11被禁止的语法会直接报错:
python
safe_eval("__import__('os').system('ls')") # ValueError: 不允许的语法: Call4. 如果只处理字面量
Python 自带ast.literal_eval:
python
import ast ast.literal_eval("{'a': [1, 2, 3]}")它只允许字面量,安全边界较窄,但不能计算变量、表达式。
5. JS 中的思路
不要用:
js
eval(expr) new Function(expr)
应使用acorn、espree等解析成 ESTree,然后白名单解释:
允许:
Literal、Identifier、BinaryExpression、LogicalExpression、UnaryExpression、ConditionalExpression禁止:
CallExpression、MemberExpression、AssignmentExpression、NewExpression、FunctionExpression、ArrowFunctionExpression、TemplateLiteral、ImportExpression、ThisExpression
6. 关键限制与结论
AST 安全求值不是“绝对安全”,只是大幅缩小攻击面。生产环境还应:
限制表达式长度、AST 节点数、执行时间、内存;
禁止
Call、Attribute、Subscript、Import、Lambda、推导式、f-string;不把模块、函数、对象、类实例传入变量环境;
必要时放到独立进程、容器、WASM 或 seccomp 沙箱中执行。
结论:
AST 安全求值 = 解析 + 白名单遍历 + 纯解释器 + 资源限制 + 默认拒绝。
复杂场景建议使用专用表达式引擎,如 CEL、JsonLogic、simpleeval、asteval等,而不是自己从零实现。