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: 不允许的语法: Call
4. 如果只处理字面量
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 等,而不是自己从零实现。