作者:Santiago Díaz(Google)
提案负责人:Shu-yu Guo(Google)
阶段:1
本提案旨在通过一种补充冻结原语的机制,以及一种使大多数代码库与其兼容的机制,来缓解一种称为原型污染的语言级漏洞。它描述了一个选择启用(opt-in)功能,该功能仅通过反射 API 提供原型。这样一来,obj[key] 语句便无法再访问原型。与此功能兼容的代码库在使用原型的方式上会更加_有意识_。
原型污染(PP)漏洞使攻击者能够操纵他们在运行时无法控制或无法访问的对象。这种“鬼魅般的超距作用”原语可用于改变其他对象的形状并覆盖其属性,从而污染运行时中的对象。
被污染的对象会使原本安全/正确的代码所依赖的底层假设失效,并可能导致任意代码执行以及 JS 代码库中各种其他安全问题。原型污染漏洞经常出现在 Web 应用程序中,但也影响非 Web 的 JS 运行时。
JS 中的对象属性可以被任何能够引用它们的代码写入。特别是,如果许多对象依赖于一个共享属性,那么其中任何一个对象都可以对所有其他对象施加更改。
原型污染的一个特殊性质是它是一种纯数据攻击,仅通过数据即可实现代码执行。例如,请参见以下易受攻击的代码以及相应的利用代码:
// source is attacker-controlled
function merge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object') {
if(target[key] === undefined) {
target[key] = {};
}
target[key] = merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// User input comes as a string
const userSuppliedObj = JSON.parse('{"__proto__": {"polluted": true}}');
// Trigger prototype pollution
merge({}, userSuppliedObj);
// Create a brand new object
const newObj = {};
// Has polluted property
console.log(newObj.polluted); // true
注意,该利用代码能够在_不注入任何外部代码_的情况下污染新对象的创建。
由于这一特殊性质,针对代码执行问题的现代缓解措施——如内容安全策略(CSP)或 Trusted Types——无法有效防御原型污染,因为它们侧重于强制执行_代码来源_。
注意,纯数据攻击适用于在 VM 上运行的代码受信任、且任意代码执行具有安全影响的情况。
freeze、seal 和 preventExtensions 的问题现有的冻结原语存在显著的设计问题,使其不太可能被广泛采用。它们对专家用户可能有用,但并不适合大多数开发者部署,因为开发者合理地期望原型是可变的:
冻结 API 存在覆盖错误和其他不一致之处,这些会在现有代码库中引入错误,导致它们抛出异常,或更糟的是在松散模式下_静默失败_。之前对覆盖错误的调查得出结论:该覆盖错误会触发约 10% 的严格模式代码库和20% 的松散模式代码库。该调查在不久后被中止。
冻结 API 给开发者带来了沉重责任:为了维护安全的代码库,需要知道哪些原型应该被冻结,这假设了开发者是安全专家。这些 API 描述了安全性的_是什么_,却没有说明_如何做_。冻结 Object 显然不够,因为许多利用都滥用 Array。那么 Error、Date、Reflect 或 Proxy 呢?或者未来的内置类型?冻结 API 无法回答这些问题。
冻结 API 假设存在一个稳定的冻结点:一个在运行时原型已稳定并可以被冻结的固定时刻。实际上,这个点是易变的,并且在积极开发的代码库中会随时间变化。虽然如今在许多应用中都能找到这样的点,但新增依赖、polyfill、代码结构变化以及热替换和开发者工具等高级功能,使冻结点成为一个移动的目标。
冻结 API 无法保护完整的原型链。在 JS 中,对象可以在任何时间点被添加到原型链中,或被从中移除。要保护整条链,人们必须始终记得冻结添加到链中的对象,这是一个容易出错的过程。当对象从链中移除后,它们就无法再被解冻了。
简而言之:一个仅向反射 API 暴露原型的特性。如果原型不通过 __proto__ 或 prototype 等属性提供,它们就不会暴露于纯数据问题。
通过一个例子可以更好地理解这一点:语句 obj[one][two] = value 可以通过 obj.__proto__.polluted 受到原型污染攻击。如果删除 Object.prototype.__proto__ 属性,同一语句就不再容易受到攻击,因为它无法适配到达原型的唯一其他方式,即 obj.constructor.prototype.polluted。请注意,prototype 无法被删除。
本提案可以通过提供反射 API 和创建一个新的选择启用(opt-in)封装特性来实现,该特性会删除原型属性。下面是对每个步骤的描述。
__proto__ 是一个可以删除的遗留属性名,但其背后的内部插槽仍然可以通过 Object/Reflect.getPrototypeOf 读取,以及通过 Object/Reflect.setPrototypeOf 写入,这只会继续让该属性对于已经运行的代码可访问。
我们建议为 prototype 创建新的 API,例如 getClassPrototypeOf 和 setClassPrototypeOf,这样就可以删除该属性名,而不会以任何方式改变这个特殊属性的工作方式,也不会影响对 VM 的支持。
反射 API 可以通过 polyfill 实现,这使得加固后的代码库能够在所有浏览器(包括旧版本)中运行。
一个新的选择启用“封装特性”,其中不为原型槽的 getter 和 setter 函数创建任何属性名,因为现在可以使用反射 API 来代替对这些属性的引用。
该特性通过一个带外标志启用:
X-Encapsulate-Prototype: true 这样的 HTTP 头--encapsulate-prototype 这样的功能标志当封装_禁用_时,原型可以通过属性和反射 API 获得。
当封装_启用_时,原型只能通过反射 API 获得,因为 __proto__ 和 prototype 都已被删除。
封装还包含以下自动重构特性:
当封装启用时,加载新源代码的 JS 引擎会在其解析阶段增加一个额外步骤,将所有对原型属性的点表示法访问注册为对其反射 API 的调用。这一步骤可以高效地实现,并允许具有第三方、传递性或动态加载依赖的代码库与封装兼容。
将来,这一改动将为把 prototype 标记为已弃用铺平道路。
当封装启用时,原型属性可以简单地是 undefined,但在尝试读取/写入它们时也可能抛出错误。这将意味着更快、更响亮地失败,并允许对反射 API/封装的迁移进行测试。
这意味着通过使用宿主钩子(host hook),让 __proto__ 和 prototype 的 getter 和 setter 根据封装状态而具有条件性,就像 eval 函数在内容安全策略下会抛出异常一样。
依赖_计算属性访问_来引用原型的代码与封装或自动重构不兼容。必须对它们进行重构,以便在使用原型时显式引用原型。这种重构实际上使代码表达了意图,使危险模式对静态分析可见。在实践中,具有此特征的代码库通常是反射框架、调试工具和其他重度反射用例,它们很可能清楚自己如何使用原型。
使用 prototype 一词定义自定义属性的代码库不兼容。如果此类代码库始终通过方括号表示法设置/获取该属性,则可以使其与封装兼容。根据 HTTP Archive 查询的历史数据,出于这个原因而不兼容的代码库占很小的比例。
对 constructor 属性的一些更改也可能产生鬼魅般的超距作用。在我们的研究过程中,我们没有发现受此影响的实际漏洞。
要使这种攻击起作用,门槛非常高:与原型污染一样,必须找到一个具有 gadget 的应用程序,既能写入又能读取任意属性。但在构造函数污染中,读取 gadget 必须从 constructor.polluted 而不是 polluted 读取。这大大减少了有用的 gadget 数量。
一些压缩后的 JS 可能与封装模式不兼容,因为静态属性访问可能被压缩为计算访问。我们查询了 HTTP Archive 以估计实际中的情况。下表显示,在过去的 12 个月中,对于所有使用桌面浏览器爬取的页面,具有此行为的页面始终低于 1%:
Google 在我们的漏洞奖励计划(Vulnerability Rewards Program)中看到漏洞提交呈上升趋势:2020 年 1 个,2021 年 3 个,2022 年至今 5 个。我们还在内部研究中发现了更多漏洞。
漏洞示例包括:
我们预计,随着 JavaScript 应用程序部署到更多环境(例如 Electron、Cloudflare Workers 等),易受攻击的应用程序数量将会增长。因此,需要一个语言级解决方案来缓解所有环境中的攻击。
| 表 | 动态访问 __proto__ 或 constructor 的文档 | 爬取的文档总数 | 比例 |
|---|
| 2023_03_01_desktop | 5,407,936 | 609,469,458 | 0.89% |
| 2023_02_01_desktop | 4,842,383 | 549,089,708 | 0.88% |
| 2023_01_01_desktop | 5,283,826 | 589,519,160 | 0.90% |
| 2022_12_01_desktop | 5,161,471 | 577,073,883 | 0.89% |
| 2022_11_01_desktop | 5,023,169 | 561,726,239 | 0.89% |
| 2022_10_01_desktop | 4,393,377 | 476,880,624 | 0.92% |
| 2022_09_01_desktop | 4,239,257 | 466,278,762 | 0.91% |
| 2022_08_01_desktop | 4,259,814 | 463,784,047 | 0.92% |
| 2022_07_01_desktop | 3,011,137 | 339,468,615 | 0.89% |
| 2022_06_01_desktop | 2,301,317 | 257,501,222 | 0.89% |
| 2022_04_01_desktop | 2,368,577 | 263,144,657 | 0.90% |
| 2022_03_01_desktop | 2,319,518 | 259,249,013 | 0.89% |