Python 最经典、也最迷人的语言哲学之一,就是“一切皆对象”:数字是对象,字符串是对象,函数是对象,类是对象,模块也是对象。对象与对象之间又通过内置属性彼此关联,比如 __class__ 指向对象所属的类,函数的 __globals__ 还能通向其定义模块的全局命名空间。再搭配 getattr 和 setattr 这样的反射机制,开发者便可以在运行时动态地访问和修改对象,这也是 Python 灵活和动态的重要来源。但从安全角度看,灵活性往往也意味着风险:一次看似普通的属性更新,如果缺乏边界限制,就可能沿着对象引用继续前进,最终影响到原本不应该被外部输入触及的运行时状态。那么,在真实 Python 代码中,这种“对象污染”究竟如何发生?我们能否系统地检测它?它又是否已经大规模存在于 Python 生态之中?

今天我们介绍的是一篇来自 Johns Hopkins University 的研究论文 The First Large-Scale Systematic Study of Python Class Pollution Vulnerability,发表在S&P 2026。这个工作第一次对 Python class pollution 漏洞进行了大规模、系统性的研究。作者不仅梳理了这类漏洞的形成机制、利用方式和安全影响,还提出了首个自动化检测框架 Pyrl,并将其应用到 GitHub 和 PyPI 上超过 600K 个真实 Python 项目中,最终发现了 47 个可利用的 zero-day class pollution 漏洞,其中包括已被 Google 和 Microsoft 确认并修复的问题。
Python对象世界里有暗门?
上图的代码片段展示了 class pollution 的一个基础示例。这个程序用于递归更新一个普通对象。例如,根据用户输入,修改 user.name 字段。但如果攻击者传入的是 {"__class__": {"__getattribute__": "1337"}},程序就会先通过 __class__ 从 user 实例走到 User 类对象,再把类上的 __getattribute__ 方法改成字符串 "1337"。这样一来,攻击者已经污染了 Python 运行时里的关键对象。后续任何对 User 实例的属性访问,都可能因为这个基础方法被污染而崩溃。我们把这个叫做class pollution vulnerability。
先找路,再下手
为了系统研究 class pollution 的各种形态及其形成机理,作者首先把“污染”这个过程拆成了两个动作:先 get,再 set。get就是攻击者能不能通过一串可控的 key,沿着对象图找到原本不该被访问的运行时对象;set就是攻击者能不能在这个对象上写入某个属性或 item。
进一步地,作者系统梳理了 Python 内建函数、标准库和常见语法中可能承担 get / set 角色的各种语义及语法形态。作者还在 50K 个最常下载的 PyPI 包上统计了这些形态的真实使用情况,说明它们并不是纸面上的语言特性,而是大量真实 Python 程序都在使用的基础操作。
基于这个观察,作者把 class pollution 系统化成了 2 × 3 的六种类型。更重要的是,在这篇工作之前,安全社区主要了解其中一种形态,而作者发现,剩下五种看起来更受限的污染形态,同样真实存在,并且依然可能造成严重后果。污染不一定需要万能钥匙,有时候一条看似受限的路径,也足够走到运行时对象图里的关键位置。
污染到哪里,才真的危险?
拆完 get 和 set 之后,作者进一步探究:攻击者改到什么对象,才真的会造成安全影响?能写入一个对象,并不等于攻击已经成功。真正危险的是那些会被程序后续读取、隐式调用,或者参与安全决策的运行时状态。
为此,作者系统整理了 Python class pollution 可能影响的目标,包括类属性、模块全局变量、函数默认参数和闭包变量等,并按照直接使用和间接使用两种方式,归类被污染对象和被污染值之间的关系。比如,攻击者可以直接污染程序后续会读取的某个字段,也可以通过 Python 的对象模型间接影响程序行为,例如污染类属性后影响所有实例的属性访问,污染函数的全局命名空间后影响模块级变量,或者污染函数默认参数后改变后续调用的语义。
最后,在污染以后造成安全后果,通常需要 gadget。这里的 gadget 指的是程序中已有的代码片段:它会在后续执行中读取被污染的值,并把这个攻击者控制的值带入安全敏感逻辑。也就是说,漏洞本身负责把污染值写进 Python 运行时对象图,而 gadget 负责把这个值“用起来”,从而劫持程序正常的数据流或控制流。不同的 gadget 会把同一个污染原语放大成不同后果:有的导致 DoS,有的触发 XSS,有的造成认证绕过或凭证泄露,而当污染值最终进入命令执行等高危逻辑时,甚至可能进一步实现 RCE。
Pyrl:用语义标签追踪污染路径
在理解 get, set, target 和 gadget 之后,下一个问题是:能不能自动把这些污染路径找出来?传统污点分析通常关心的是用户输入有没有流到 eval 这样的危险 sink。但对 class pollution 来说,真正危险的不只是写入的值可控,而且被写入的对象的解析过程也可控。换句话说,要检测 class pollution,不能只问“输入有没有流到某个对象”,而要问“输入有没有参与对象解析,并最终导致一个攻击者可达的对象被修改”。
为了解决这个问题,作者设计了 Pyrl,并提出一种新的静态分析方法:operational taint analysis。和传统污点分析只追踪一条 source-to-sink flow 不同,Pyrl 同时建模多条相互交织的污染路径:用户输入如何变成 pollution key,这串 key 又如何通过 get 操作一步步解析出目标运行时对象,以及污染值如何流向最终写入点。Pyrl 使用细粒度的语义标签来区分每个值在程序中的操作角色,例如,原始输入、路径中的 key、还是由 key 通过 attribute get / item get 解析得到的 object。最后,Pyrl 再检查这些 key, value 和 target object 是否在 set 操作处汇合,从而判断这是不是一条真正的 class pollution 路径。
Python对象世界的“穹顶之下”
为了回答“class pollution是否已大规模存在于python生态中”这个问题,作者进一步将研究从漏洞原理验证扩展到了大规模生态扫描。他们收集了 GitHub 上超过 100 stars 的 Python 项目,以及 PyPI 上的可用包,总计超过 60 万个真实项目。并在这个数据集上使用Pyrl进行检测。最终,Pyrl 报告了 868 个潜在漏洞,作者人工检查其中 84 个,并确认了 47 个可利用的 zero-day class pollution 漏洞。
另一个值得注意的发现是,真实生态里最普遍的 class pollution,并不是此前已知的最强形态,即 Agnostic-Get × Dual-Set:攻击者既能灵活地沿对象图寻找目标,又能灵活完成写入。在这篇工作之前,社区对 class pollution 的理解基本停留在这一类。而 Pyrl 的结果显示,在现实生态中占比最高的反而是作者新发现的受限形态:攻击者只能按照程序固定的方式 get,也只能通过 attribute set 完成写入,也就是 Constrained-Get × Attr-Set。真实世界里最多的并不是“万能钥匙”式的完全自由污染,而是看起来更弱、更窄的污染路径。但“受限”并不等于“不危险”:为了验证这一点,作者进一步利用 Taipy 案例中的漏洞利用,证明即使是这种能力最弱、限制最多的 class pollution,也依然能够被 gadget 链放大,最终实现 RCE。
最后,作者还做了一个 class pollution 的 Wiki 网页,整理了漏洞背景、论文、检测工具和数据集。如果你读到这里觉得这个问题挺有意思,欢迎点进去继续看看。也欢迎顺手给我们的 GitHub 仓库点个 Star!
- Wiki: https://class-pollution.github.io/
- Paper: https://jackfromeast.github.io/assets/Pyrl.pdf
- Tool&Dataset: https://github.com/jackfromeast/python-class-pollution