eval 与 exec 的基本用法
慎用Python eval/exec:动态执行代码的潜在安全风险与替代方案
在Python编程中,eval() 和 exec() 是两个强大的内置函数,它们允许程序在运行时动态地执行字符串形式的代码。这种能力看似灵活高效,尤其在处理用户输入、配置脚本或实现小型解释器时颇具吸引力。然而,正是这种“灵活性”背后潜藏着巨大的安全隐患和不可预测性。本文将深入剖析 eval() 与 exec() 的工作原理、典型风险场景,并提供更安全、更可控的替代方案,帮助开发者在享受动态执行便利的同时,规避潜在陷阱。
eval 与 exec 的基本用法
eval() 用于求值表达式(expression),并返回结果;而 exec() 用于执行语句(statement),不返回值。例如:
# eval 示例:计算数学表达式
result = eval("2 + 3 * 4")
print(result) # 输出 14
# exec 示例:执行赋值语句
code = """
x = 10
y = x * 2
print(f'y = {y}')
"""
exec(code)
表面上看,这些功能非常方便。但问题在于:当传入的字符串来源于不可信的外部输入(如用户输入、网络请求、配置文件等)时,程序就相当于把执行权交给了攻击者。

安全风险:从信息泄露到系统沦陷
假设你正在开发一个简单的计算器Web应用,允许用户输入数学表达式:
# 危险示例:直接使用 eval 处理用户输入
user_input = input("请输入表达式: ")
result = eval(user_input)
print("结果:", result)
如果用户输入的是 "__import__('os').system('rm -rf /')"(在类Unix系统上)或 "__import__('subprocess').run(['del', 'C:\\'], shell=True)"(在Windows上),后果不堪设想。即使没有删除文件,攻击者也可以通过以下方式获取敏感信息:
# 尝试读取环境变量
eval("__import__('os').environ")
# 列出当前目录文件
eval("__import__('os').listdir('.')")
更隐蔽的攻击可能利用 eval 访问全局或局部命名空间中的变量,窃取会话令牌、数据库连接信息等。由于 eval 默认使用当前作用域的 globals() 和 locals(),任何未加限制的调用都可能暴露内部状态。
限制执行环境:真的安全吗?
一些开发者试图通过限制 globals 和 locals 来“沙箱化” eval,例如:
# 表面安全的尝试
safe_dict = {"__builtins__": {}}
result = eval(user_input, safe_dict, {})
然而,这种做法依然存在漏洞。例如,某些对象的方法可能被滥用,或者通过 ().__class__.__base__.__subclasses__() 等方式绕过限制(尤其在旧版Python中)。Python官方文档明确指出:eval() 和 exec() 无法在不可信输入下安全使用,即使设置了受限的命名空间。
此外,exec() 的风险更高,因为它可以执行任意语句,包括定义函数、导入模块、修改变量等,几乎等同于在你的程序中嵌入了一个完整的Python解释器。
性能与可维护性问题
除了安全风险,eval/exec 还带来其他问题:
- 性能开销:每次调用都需要解析和编译字符串为字节码,效率远低于直接执行代码。
- 调试困难:错误堆栈难以追踪,异常信息模糊,不利于排查问题。
- 代码可读性差:动态生成的逻辑隐藏在字符串中,破坏了静态分析和IDE支持。
更安全的替代方案
面对动态执行的需求,我们应优先考虑以下更安全、更清晰的替代方法:
1. 使用专用解析器处理表达式
对于数学表达式、简单逻辑判断等场景,可使用专门的表达式求值库(如 ast.literal_eval 仅支持字面量,或第三方库如 simpleeval)。若需自定义,可基于 ast 模块构建安全解析器:
import ast
import operator
# 安全的数学表达式求值器
def safe_eval(expr):
ops = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.USub: operator.neg,
}
def _eval(node):
if isinstance(node, ast.Constant): # Python 3.8+
return node.value
elif isinstance(node, ast.Num): # 兼容旧版本
return node.n
elif isinstance(node, ast.BinOp):
return ops[type(node.op)](_eval(node.left), _eval(node.right))
elif isinstance(node, ast.UnaryOp):
return ops[type(node.op)](_eval(node.operand))
else:
raise TypeError(f"不支持的操作: {node}")
tree = ast.parse(expr, mode='eval')
return _eval(tree.body)
# 使用示例
print(safe_eval("2 + 3 * 4")) # 输出 14
# safe_eval("__import__('os')") # 抛出异常
该方法仅允许预定义的操作,彻底杜绝了任意代码执行。
2. 使用配置文件代替代码执行
若需动态配置行为,应使用 JSON、YAML 或 INI 等结构化格式,而非可执行代码:
import json
# 从配置文件加载参数
with open("config.json") as f:
config = json.load(f)
# 根据配置决定行为
if config["mode"] == "debug":
enable_debug()
这种方式不仅安全,还便于版本控制和验证。
3. 利用回调函数或策略模式
当需要根据输入选择不同逻辑时,可预定义函数映射:
def add(a, b): return a + b
def mul(a, b): return a * b
operations = {
"add": add,
"mul": mul,
}
op_name = input("操作 (add/mul): ")
if op_name in operations:
result = operations[op_name](2, 3)
else:
raise ValueError("不支持的操作")
这避免了动态执行,同时保持了灵活性。
结语
eval() 和 exec() 是Python中极具威力的工具,但正如一句老话所说:“能力越大,责任越大。”在绝大多数实际应用场景中,它们所带来的风险远大于便利。开发者应始终秉持“最小权限”原则,对任何来自外部的输入保持警惕。通过采用专用解析器、结构化配置或设计模式,我们完全可以在不牺牲功能的前提下,构建更安全、更健壮的应用程序。记住:永远不要信任用户输入,更不要让它变成你的代码。慎用 eval 与 exec,是每一位Python开发者应有的安全意识。

