eval 与 exec 的基本用法

01-24 3711阅读

慎用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)

表面上看,这些功能非常方便。但问题在于:当传入的字符串来源于不可信的外部输入(如用户输入、网络请求、配置文件等)时,程序就相当于把执行权交给了攻击者

eval 与 exec 的基本用法

安全风险:从信息泄露到系统沦陷

假设你正在开发一个简单的计算器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(),任何未加限制的调用都可能暴露内部状态。

限制执行环境:真的安全吗?

一些开发者试图通过限制 globalslocals 来“沙箱化” 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中极具威力的工具,但正如一句老话所说:“能力越大,责任越大。”在绝大多数实际应用场景中,它们所带来的风险远大于便利。开发者应始终秉持“最小权限”原则,对任何来自外部的输入保持警惕。通过采用专用解析器、结构化配置或设计模式,我们完全可以在不牺牲功能的前提下,构建更安全、更健壮的应用程序。记住:永远不要信任用户输入,更不要让它变成你的代码。慎用 evalexec,是每一位Python开发者应有的安全意识。

文章版权声明:除非注明,否则均为Dark零点博客原创文章,转载或复制请以超链接形式并注明出处。

目录[+]

Music