Python 如何安全地执行用户输入的表达式(不要用 eval)

不能用 eval 执行用户输入,因其会直接执行任意代码、无沙箱限制;应优先用 ast.literal_eval 处理安全字面量,必要时用 simpleeval 或自定义 AST 遍历限制运算符和节点。

为什么不能用 eval 执行用户输入

eval 会直接在当前作用域执行任意 Python 代码,哪怕只是传入 "__import__('os').system('rm -rf /')" 这样的字符串,也可能触发系统调用、文件操作或网络请求。它不区分“表达式”和“语句”,也不限制内置函数访问,本质上不是沙箱——只是把字符串当代码扔进解释器里跑一遍。

真实场景中,用户可能故意输入 "2 + (lambda: exec('print(1)'))()" 绕过简单关键词过滤;或者利用 getattr__builtins__ 等反射机制逃逸限制。靠字符串黑名单/白名单基本防不住。

ast.literal_eval 处理安全子集

ast.literal_eval 只允许基础字面量:数字、字符串、元组、列表、字典、布尔值、None。它不会执行函数调用、属性访问、运算符重载,也不会触发任何副作用。

  • 适合场景:解析用户提交的配置项、JSON-like 参数(如 "[1, 2, {'x': 3.14}]"
  • 不支持:变量名、算术运算("2 +

    3"
    )、比较("5 > 3")、函数调用("len([1,2])"
  • 错误类型:输入非法时抛出 ValueErrorSyntaxError,不是 Exception 的宽泛捕获

示例:

import ast
try:
    result = ast.literal_eval("[1, 2, {'a': True}]")
except (ValueError, SyntaxError) as e:
    # 安全失败,无副作用
    print("Invalid literal:", e)

需要计算表达式?用 simpleeval 或自定义 AST 遍历

如果必须支持 "2 * x + 1" 这类带变量和运算的表达式,ast.literal_eval 不够用,但又不想自己从头写解析器,推荐轻量库 simpleeval

  • 默认禁用所有函数和属性访问,只开放基础运算符和常量
  • 可安全注入变量:se.eval("a + b", names={"a": 10, "b": 20})
  • 能限制最大执行步数、嵌套深度、字符串长度,防止穷举或栈溢出
  • 不支持 lambdaimportexec、下标以外的属性访问(如 obj.method

不用第三方库时,可基于 ast.Expression + 白名单节点遍历实现最小计算器,重点拦截 ast.Callast.Attributeast.Subscript(除非明确允许数组索引)等危险节点。

绕过沙箱的常见手法与应对

即使用了 simpleeval 或自定义 AST,也要警惕用户构造边缘输入:

  • "1 if True else __import__('sys').exit()" → 检查是否允许条件表达式,或禁用 ast.IfExp
  • "'a' * 10**6" → 限制字符串生成长度,或在遍历时统计总字符估算量
  • "[i for i in range(10**6)]" → 禁用列表推导式(ast.ListComp),或限制循环模拟次数
  • 利用大整数幂运算耗尽 CPU:9**9**5 → 在访客节点时检查数字大小和运算符组合

真正安全的表达式求值没有银弹。越接近通用计算能力,防御成本越高。多数业务场景其实只需要 ast.literal_eval 或固定几个运算符的白名单——先想清楚“用户到底需要算什么”,再决定沙箱复杂度。