
1. 从一行“危险”的代码说起如果你在Python社区里混过一段时间大概率听过一句忠告“永远不要使用eval”。这个警告就像编程界的“不要随便捡路上的钱”一样被反复提及。我第一次真正理解这句话的分量是在一个深夜的线上故障排查中。当时一个运行了半年的后台服务突然CPU飙高日志里疯狂刷出数据库连接耗尽的错误。追根溯源问题出在一个看似无害的配置解析模块里。为了“灵活”开发者用eval来解析用户从管理后台输入的字符串将其转换为Python字典。直到某天一个测试人员在配置里输入了__import__(‘os’).system(‘rm -rf /tmp/*’)虽然因为权限问题没造成实际删除但这个“死循环”式的系统调用直接把服务拖垮了。这件事让我意识到eval远不止是一个简单的“字符串求值”工具它是一把极其锋利、且没有刀鞘的双刃剑。那么eval到底是什么简单说它是一个内置函数用于执行一个字符串表达式并返回表达式的值。听起来人畜无害对吧eval(“3 5”)返回8eval(“[i*2 for i in range(5)]”)返回[0, 2, 4, 6, 8]。这种能力让它在某些特定场景下显得无比强大和便捷比如快速计算数学公式、动态执行少量代码、或者作为简易的配置解析器。这也是为什么尽管警告声不绝于耳我们依然能在一些开源项目甚至生产代码的角落里发现它的身影。然而它的危险性正源于这种强大的能力。eval会在当前命名空间中执行传入的字符串。这意味着如果这个字符串来自不可信的来源比如用户输入、网络请求、外部文件那么它几乎可以执行任何Python代码导入危险模块、删除文件、访问敏感数据、甚至利用系统漏洞。更隐蔽的风险在于即使你做了简单的字符串检查高明的攻击者也能通过拼接、编码或利用Python的魔法方法如__builtins__绕过限制。因此深入理解eval不仅仅是学会怎么用它更重要的是厘清它的能力边界、安全风险以及知道在什么情况下绝对不能用在什么情况下可以谨慎使用以及有哪些更安全的替代方案。2.eval的核心机制与能力边界要安全地使用或避免使用一个工具首先得彻底弄明白它是怎么工作的。eval的函数签名是eval(expression, globalsNone, localsNone)。这三个参数共同决定了这段动态代码的执行环境和权限。2.1 参数解析globals与locals的权限沙箱expression就是要执行的字符串。而globals和locals是两个字典分别代表全局和局部命名空间。eval执行时会在这两个字典构成的命名空间中查找变量。默认情况下的“全权委托”当你调用eval(“x y”)而不指定后两个参数时eval会在调用者当前的全局和局部命名空间中查找x和y。这意味着它拥有当前代码的所有权限能访问所有全局变量、导入的模块、甚至内置函数。x 10 y 20 result eval(“x * y”) # 结果为200因为它能访问到外部的x和y这非常危险因为如果表达式字符串来自用户用户就可以通过__import__(‘os’).system(‘命令’)来调用系统命令。构建安全的沙箱环境为了限制eval的能力我们必须显式地传入globals和locals参数构建一个受控的沙箱。# 创建一个空的全局命名空间并限制内置函数 safe_globals {“__builtins__”: {}} # 清空内置函数这是关键一步 safe_locals {“a”: 5, “b”: 3} # 尝试执行 try: result eval(“a b”, safe_globals, safe_locals) print(result) # 输出 8因为a和b在locals中定义了 except NameError as e: print(e) # 如果表达式里用了未定义的变量会抛出NameError # 尝试执行危险代码 try: eval(“__import__(‘os’).system(‘ls’)”, safe_globals, {}) except Exception as e: print(f”被安全沙箱拦截: {e}“) # 会抛出NameError因为__import__不存在这里的关键操作是{“__builtins__”: {}}。在Python中__builtins__模块包含了所有内置函数如len,open,__import__。将它设置为空字典就从根本上剥夺了eval执行绝大多数危险操作的能力。这是构建安全沙箱的基石。注意即使设置了{“__builtins__”: {}}攻击者仍有可能通过Python对象继承链等极其复杂的方式逃逸沙箱例如如果沙箱内存在某个类其基类可能指向外部对象。因此绝对安全的沙箱非常难构建。对于处理完全不可信的输入最好的办法是永远不用eval。2.2eval与exec、compile的兄弟关系经常和eval一起被提及的还有exec和compile。理解它们的区别能帮你更精准地选择工具。eval: 用于计算单个表达式的值并返回结果。表达式可以是任何能被求值为单个对象的Python代码比如“35”、“[x for x in range(10)]”、“a if a b else b”。它不能执行语句如赋值a1、循环for i in range(10):、函数定义def foo():。exec: 用于动态执行Python代码块可以是语句、循环、函数定义等。它不返回任何值或者说返回None它的作用是执行代码带来的副作用如修改变量、定义函数。compile: 是一个更底层的函数它将源代码字符串编译为代码对象code object这个代码对象可以随后被eval()或exec()执行。compile允许你指定代码的模式‘eval’ ‘exec’ 或 ‘single’这决定了这段代码是被当作表达式、语句块还是交互式单条命令来编译。# eval 示例 expr “10 * 20 30” value eval(expr) # value 230 # exec 示例 code_block “”” def greet(name): return f“Hello, {name}!” print(greet(“World”)) “”” exec(code_block) # 输出 “Hello, World!”并定义了greet函数 # compile eval 示例 expr “6 * 7” code_obj compile(expr, ‘string’, ‘eval’) # 模式必须是 ‘eval’ result eval(code_obj) # result 42简单来说当你只需要一个计算结果时用eval当你需要运行一段有逻辑的代码时用exec当你需要对代码进行预处理比如检查语法、重复执行时会用到compile。从安全角度看exec通常比eval更危险因为它能执行的功能更强大、更完整。3.eval的典型应用场景与安全实践既然eval这么危险为什么它还存在因为它在某些受控的、内部使用的场景下确实能提供无与伦比的便利性。关键在于我们必须严格界定这些场景并实施最高等级的安全措施。3.1 场景一动态公式计算器受控环境这是eval最经典也相对最安全的用途。例如你开发了一个科学计算器应用或者一个数据分析工具允许用户输入像“sin(pi/4) * sqrt(2)”这样的数学公式。安全实践步骤严格限定命名空间只导入数学计算所需的模块和函数。彻底禁用内置函数这是防止逃逸的核心。输入清洗与白名单校验在求值前对输入字符串进行语法分析和过滤。import math def safe_eval_math(formula: str) - float: “”” 安全地计算数学公式。 仅允许使用 math 模块中的函数和常量以及基本的运算符和数字。 “”” # 1. 定义安全的全局命名空间 safe_dict { “__builtins__”: {}, # 禁用所有内置函数 “math”: math, # 允许使用math模块 “sin”: math.sin, “cos”: math.cos, “tan”: math.tan, “sqrt”: math.sqrt, “log”: math.log, “exp”: math.exp, “pi”: math.pi, “e”: math.e, # … 可以按需添加其他math函数 } # 2. (可选但推荐) 使用AST进行语法树检查实现白名单 import ast try: tree ast.parse(formula, mode‘eval’) except SyntaxError: raise ValueError(“无效的公式语法”) # 一个简单的节点类型检查器只允许常量、数学运算、函数调用仅限白名单函数名 allowed_node_types (ast.Expression, ast.BinOp, ast.UnaryOp, ast.Constant, ast.Call, ast.Name, ast.Load, ast.Add, ast.Sub, ast.Mult, ast.Div, ast.Pow, ast.USub, ast.UAdd) for node in ast.walk(tree): if not isinstance(node, allowed_node_types): raise ValueError(f”公式中包含不允许的语法结构: {type(node).__name__}“) # 进一步检查函数调用只允许调用safe_dict中存在的函数 if isinstance(node, ast.Call): if not isinstance(node.func, ast.Name): raise ValueError(“不允许复杂的调用表达式”) if node.func.id not in safe_dict: raise ValueError(f”不允许调用函数 ‘{node.func.id}’”) # 3. 在沙箱中执行 try: return eval(formula, {“__builtins__”: {}}, safe_dict) except Exception as e: # 捕获所有执行期异常如除零错误、数学域错误等 raise ValueError(f”公式计算错误: {e}“) # 测试 print(safe_eval_math(“3 5 * 2”)) # 13.0 print(safe_eval_math(“sqrt(16) sin(pi/2)”)) # 5.0 try: safe_eval_math(“__import__(‘os’).system(‘ls’)”) except ValueError as e: print(e) # 会被AST检查或执行时的NameError拦截这个实现结合了命名空间限制和抽象语法树AST白名单检查构成了双重保险。AST检查能在执行前就发现并拒绝危险的代码结构比单纯依赖执行时的命名空间限制更主动、更安全。3.2 场景二简易配置解析内部可信数据有时我们可能希望配置文件不仅仅是YAML或JSON的键值对而能支持简单的Python表达式以便引用其他配置项或进行动态计算。例如base_dir “/opt/app”log_file f“{base_dir}/logs/app.log”。安全实践前提是配置文件完全由内部运维或开发人员编写且存储位置安全不可被外部用户篡改。在这种情况下安全风险从“代码注入”转变为“配置错误可能导致代码执行异常”。import json def parse_config_with_eval(config_path: str): with open(config_path, ‘r’) as f: config_data json.load(f) # 假设基础结构是JSON # 定义一个非常有限的命名空间或许只包含一些字符串处理方法 safe_globals {“__builtins__”: {“str”: str, “len”: len}} # 按需添加极少数安全内置函数 # 可以预定义一些配置中可能用到的常量或简单函数 safe_globals[“env”] “production” # 示例环境变量 parsed_config {} for key, value in config_data.items(): if isinstance(value, str) and value.startswith(“eval:”): # 仅处理显式标记为需要eval的字符串 expr value[5:].strip() # 去掉 “eval:” 前缀 try: parsed_value eval(expr, safe_globals, {}) parsed_config[key] parsed_value except Exception as e: raise ValueError(f”解析配置项 ‘{key}’ 的表达式 ‘{expr}’ 时出错: {e}“) else: parsed_config[key] value return parsed_config # config.json 内容可能为 # { # “timeout”: “eval: 30 * 2”, # “retry_times”: “eval: 3 if env ‘production‘ else 5” # }重要警告即使对于内部配置使用eval也需极其谨慎。一个拼写错误如eval: import os就会导致解析失败。更好的做法是设计一种更安全、表达能力稍弱的迷你语言DSL或者直接使用成熟的模板引擎如Jinja2并严格限制其环境。3.3 场景三教育演示与原型开发在编写教程、进行教学演示或者快速验证一个算法想法时eval可以快速将字符串形式的代码片段变为可运行的状态非常方便。例如在交互式笔记如Jupyter Notebook中快速测试。安全实践此场景的核心是“环境隔离”。确保这个用于演示的Python环境是临时的、干净的、与生产系统隔离的。即便eval执行了危险代码影响的也仅仅是这个临时环境。# 在一个全新的、隔离的虚拟环境中进行演示 print(“演示动态生成列表推导式”) code “[x**2 for x in range(10)]” print(f”代码字符串: {code}“) result eval(code) print(f”执行结果: {result}“)在这种场景下风险是可控的因为执行者就是开发者自己且环境是隔离的。但这也提醒我们从教学伊始就应该向学习者强调eval的危险性养成良好的安全习惯。4. 为什么说“永远不要用eval”—— 安全风险深度剖析社区里“永远不要用eval”的呼声主要针对的是处理不可信输入的场景。我们来深入剖析几个即使你自认为做了防护也可能被攻破的案例。4.1 绕过简单字符串过滤新手常犯的错误是试图用字符串替换replace或关键字黑名单来过滤危险代码。# 危险极易被绕过的过滤 user_input “__import__(‘os’).system(‘ls’)” # 恶意输入 # 天真地过滤 if “import” in user_input or “os” in user_input or “system” in user_input: print(“检测到危险关键词”) else: result eval(user_input) # 千万不要这么做攻击者可以轻松绕过字符串拼接“__imp” “ort__(‘os’).syst” “em(‘ls’)”编码混淆使用chr()函数动态生成字符。eval(“__import__(‘os’).system(“ chr(108) chr(115) “)”)其中chr(108)是 ‘l’chr(115)是 ‘s’。利用属性访问getattr(__builtins__, ‘__imp’’ort__’)(‘os’).system(‘ls’)只要解释器最终能理解攻击者有无数种方法混淆代码简单的文本匹配完全无效。4.2 沙箱逃逸__builtins__的陷阱前面我们提到用{“__builtins__”: {}}来构建沙箱。但这并非绝对安全。案例通过类继承链访问原始__builtins__如果在沙箱内用户输入能创建或访问一个类就有可能通过类的继承关系__bases__,__mro__或特殊属性__globals__触及到外部的、未被清理的命名空间。# 一个不安全的沙箱示例展示了潜在风险 safe_globals {“__builtins__”: {}} # 不小心在沙箱里暴露了一个来自外部的类 class ExternalClass: pass safe_globals[‘SomeClass’] ExternalClass # 攻击者可能尝试的输入概念性演示实际构造更复杂 # 通过 SomeClass.__bases__ 等属性向上摸索可能找到 object 类 # 而 object.__subclasses__() 可以返回所有子类其中可能包含有危险能力的类。 # 这需要极深的Python内部知识但理论上是可能的。因此一个真正安全的沙箱不仅需要清空__builtins__还需要确保沙箱内不暴露任何来自外部、可能成为“跳板”的对象。这非常困难也是安全专家建议避免自制沙箱的原因。4.3 资源耗尽与拒绝服务攻击即使不考虑代码执行eval也可能被用于发起拒绝服务DoS攻击。无限循环eval(“while True: pass”)会立刻卡死当前线程。内存爆炸eval(“[0] * (10**8)”)会尝试创建一个包含一亿个元素的列表瞬间消耗大量内存。递归爆炸eval(“def f(): f()\nf()”)会导致递归深度超过限制而崩溃。在Web服务等场景下攻击者无需获取系统权限仅通过提交这类消耗资源的“表达式”就能让你的服务瘫痪。5. 安全替代方案彻底抛弃eval鉴于eval的高风险在绝大多数情况下我们都有更好、更安全的替代方案。5.1 数学表达式计算使用专用库对于“动态公式计算器”场景有现成的、安全的库ast.literal_eval(): Python标准库函数。它只能评估“字面量”表达式即Python的基础数据结构字符串、字节串、数字、元组、列表、字典、集合、布尔值、None。它完全不能执行函数或方法调用因此是绝对安全的。但它不能计算“3 5”这样的算术表达式。numexpr: 用于高效计算数值表达式如“sin(a) * 3 log(b)”它有自己的安全解析器和执行引擎不调用Python的eval。pandas.eval(): 在Pandas的DataFrame上下文中进行表达式求值经过安全处理。sympy: 符号数学库可以安全地解析和计算数学表达式。import ast import numexpr as ne import pandas as pd # 1. ast.literal_eval - 安全地解析数据结构 safe_data ast.literal_eval(‘[1, 2, {“key”: “value”}]’) # 正常工作 # safe_data ast.literal_eval(‘__import__(“os”).system(“ls”)’) # 会抛出 SyntaxError 或 ValueError # 2. numexpr - 安全地计算数学表达式 a, b 10, 20 result ne.evaluate(“a * b sin(pi/2)”) # 需要提前定义变量到命名空间 print(result) # 3. pandas.eval - DataFrame上的安全计算 df pd.DataFrame({‘A’: [1, 2, 3], ‘B’: [4, 5, 6]}) result_series df.eval(‘A * 2 B’) print(result_series)5.2 配置与模板使用模板引擎或DSL对于需要动态内容的配置或模板应该使用专门的工具字符串格式化对于简单的变量替换str.format()或 f-string 足够。模板引擎如Jinja2常用于Web框架。它可以配置一个非常严格的沙箱环境禁用不安全的函数和过滤器比你自己用eval构建沙箱要可靠得多。自定义迷你语言DSL如果需求复杂可以定义一套简单的语法并自己编写解析器。虽然开发成本高但安全性完全可控。Python的pyparsing或lark库可以帮助你快速构建DSL解析器。5.3 动态代码执行重构设计避免动态性很多时候感觉需要eval或exec其实是设计上可以优化的信号。问问自己是否可以用多态/策略模式代替将不同的逻辑封装在不同的函数或类中通过字典映射或工厂模式来动态选择而不是拼接代码字符串。是否可以用回调函数或插件架构让用户提供符合接口的函数对象而不是代码字符串。数据是否应该与代码分离用结构化的数据JSON, YAML配合固定的处理逻辑远比执行任意代码安全。6. 实战构建一个相对安全的表达式求值器尽管有诸多警告但为了彻底理解如何约束eval我们来尝试设计一个用于数学表达式求值的“相对安全”的工具。我们将结合AST检查、严格的白名单和资源限制。import ast import math import operator import time import resource class SafeMathEval: “”” 一个相对安全的数学表达式求值器。 仅支持基础算术、比较、逻辑运算以及白名单内的数学函数和常量。 “”” # 定义允许的运算符映射 _allowed_operators { ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv, ast.Pow: operator.pow, ast.USub: operator.neg, ast.UAdd: operator.pos, ast.Eq: operator.eq, ast.NotEq: operator.ne, ast.Lt: operator.lt, ast.LtE: operator.le, ast.Gt: operator.gt, ast.GtE: operator.ge, } # 定义白名单函数和常量 _safe_globals { “abs”: abs, “round”: round, “min”: min, “max”: max, “sum”: sum, “math”: math, # 提供整个math模块但会在AST中检查具体调用 } _safe_constants { “pi”: math.pi, “e”: math.e, } def __init__(self, max_expr_length100, timeout1.0): self.max_expr_length max_expr_length self.timeout timeout def eval(self, expression: str): “””安全地求值数学表达式。””” # 1. 基础长度检查 if len(expression) self.max_expr_length: raise ValueError(f”表达式过长超过{self.max_expr_length}字符”) # 2. 编译为AST语法检查 try: tree ast.parse(expression, mode‘eval’) except SyntaxError as e: raise ValueError(f”表达式语法错误: {e}“) # 3. 遍历AST进行安全检查和白名单验证 self._check_ast(tree.body) # 4. 准备安全的执行环境 eval_globals {“__builtins__”: {}} eval_globals.update(self._safe_globals) eval_globals.update(self._safe_constants) # 5. 设置执行超时使用信号或轮询此处简化演示 start_time time.time() # 注意简单的time.time()检查无法中断长时间CPU操作生产环境需用signal或multiprocessing def check_timeout(): if time.time() - start_time self.timeout: raise TimeoutError(“表达式计算超时”) # 6. 编译并执行 code_obj compile(tree, ‘string’, ‘eval’) try: result eval(code_obj, eval_globals, {}) check_timeout() return result except TimeoutError: raise except Exception as e: # 捕获所有其他执行异常如除零错误、数学域错误 raise ValueError(f”表达式计算错误: {e}“) def _check_ast(self, node): “””递归检查AST节点只允许白名单内的结构。””” if isinstance(node, ast.Expression): return self._check_ast(node.body) # 允许的节点类型 allowed_types ( ast.Constant, # 数字、字符串等常量 ast.BinOp, # 二元运算 ast.UnaryOp, # 一元运算 ast.Compare, # 比较运算 ast.BoolOp, # 逻辑运算 (and, or) ast.Call, # 函数调用 ast.Name, # 变量名 ast.Subscript, # 索引、切片如果允许的话 ast.Attribute, # 属性访问如 math.sqrt ) if not isinstance(node, allowed_types): raise ValueError(f”不允许的语法结构: {type(node).__name__}“) # 具体检查 if isinstance(node, ast.Call): # 只允许调用白名单函数 if isinstance(node.func, ast.Name): if node.func.id not in self._safe_globals: raise ValueError(f”不允许调用函数 ‘{node.func.id}’”) elif isinstance(node.func, ast.Attribute): # 允许 math.sqrt 这种形式 if not (isinstance(node.func.value, ast.Name) and node.func.value.id ‘math’): raise ValueError(“只允许调用 math 模块下的函数”) if node.func.attr not in dir(math): raise ValueError(f”math 模块没有函数 ‘{node.func.attr}’”) else: raise ValueError(“不允许复杂的调用表达式”) # 递归检查参数 for arg in node.args: self._check_ast(arg) for kw in node.keywords: self._check_ast(kw.value) elif isinstance(node, ast.Name): # 只允许白名单常量 if node.id not in self._safe_constants and node.id not in self._safe_globals: raise ValueError(f”未定义的变量或常量 ‘{node.id}’”) # 递归检查子节点 for child in ast.iter_child_nodes(node): self._check_ast(child) # 使用示例 calculator SafeMathEval() print(calculator.eval(“3 5 * 2”)) # 13 print(calculator.eval(“sqrt(16)”, {“sqrt”: math.sqrt})) # 4.0, 需要传入函数 print(calculator.eval(“pi * 2”)) # 6.283185307179586 try: calculator.eval(“__import__(‘os’).system(‘ls’)”) except ValueError as e: print(f”安全拦截: {e}“) try: calculator.eval(“[1,2,3]”) # literal_eval可以但我们这里不允许复杂数据结构 except ValueError as e: print(f”安全拦截: {e}“)这个SafeMathEval类展示了构建一个“相对安全”的求值器需要考虑的多个层面输入长度限制、语法树白名单检查、严格的执行命名空间、以及执行时间限制。它比单纯的{“__builtins__”: {}}要健壮得多。然而我必须再次强调“相对安全”不等于“绝对安全”。对于处理来自互联网的、完全不可信的输入最负责任的做法仍然是寻找无需eval的替代方案。这个练习的价值在于让你透彻理解eval的风险究竟在哪里以及安全防护的复杂性从而在未来做出更明智的技术选型。当你不得不面对一段遗留代码中的eval时你也知道该如何去审计和加固它。