For AI agents: the complete documentation index is available at /tc39-atlas/llms.txt, the full documentation bundle is available at /tc39-atlas/llms-full.txt, and this page is available as Markdown at /tc39-atlas/proposals/stage/1/proposal-symbol-proto.md.
  • 简体中文
  • Prototype pollution mitigation S1

    中文标题:原型污染缓解

    提案概览
    提案速览

    本提案解决原型污染问题,这是一种语言级漏洞,攻击者可操纵对象原型来改变行为。它提出了一个选择加入的功能,仅通过反射 API 暴露原型,并提供自动重构以处理遗留代码。

    Note

    以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。

    原型污染缓解 / Symbol.proto

    作者Santiago Díaz (谷歌)

    倡导者:Shu-yu Guo (谷歌)

    阶段:1

    目录

    摘要

    本提案旨在通过一种机制来缓解称为原型污染的语言级漏洞,该机制补充了冻结原语,并提供了一种使大多数代码库与之兼容的机制。它描述了一个选择加入的功能,使得原型只能通过反射 API 访问。这样,语句 obj[key] 就无法再访问原型了。与此功能兼容的代码库在使用原型方面更具_目的性_。

    问题描述

    超距作用

    原型污染漏洞允许攻击者操纵那些他们在运行时无法控制或无法访问的对象。这种“超距作用”原语可用于改变其他对象的形状并覆盖其属性,从而污染运行时中的对象。

    被污染的对象会使原本安全/正确的代码的底层假设失效,并可能导致任意代码执行和 JS 代码库中的各种其他安全问题。原型污染漏洞经常在 Web 应用中表现出来,但也影响非 Web 的 JS 运行时。

    JS 中的对象属性可以被任何能够引用它们的代码写入。特别是,如果许多对象依赖于一个共享属性,那么其中任何一个都可以对所有其他对象实施更改。

    仅数据攻击

    原型污染的一个特殊属性是它是一种仅数据攻击,允许仅通过数据即可实现代码执行。例如,请看以下易受攻击的代码和相应的利用方式:

    // source 是攻击者控制的
    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;
    }
    
    // 用户输入以字符串形式出现
    const userSuppliedObj = JSON.parse('{"__proto__": {"polluted": true}}');
    // 触发原型污染
    merge({}, userSuppliedObj);
    // 创建一个全新的对象
    const newObj = {};
    // 具有被污染的属性
    console.log(newObj.polluted); // true

    请注意,该利用方式能够在_不注入任何外部代码_的情况下污染新对象的创建。

    由于此特殊属性,针对代码执行问题的现代缓解措施(如内容安全策略或可信类型)在防范原型污染方面显得不足,因为它们专注于强制_代码来源_。

    注意:仅数据攻击与 VM 上运行的代码受信任且任意代码执行具有安全影响的情况相关。

    freezesealpreventExtensions 的问题

    现有的冻结原语存在显著的设计问题,使得它们不太可能被广泛采用。它们对专家用户可能有用,但不太适合大多数开发者部署,他们合理地期望原型是可变的

    覆盖错误

    冻结 API 受到覆盖错误和其他不一致性的影响,这些会导致现有代码库中出现错误,使其抛出异常,或者更糟糕的是,在宽松模式下_静默失败_。先前对覆盖错误的调查得出结论,覆盖错误在约 10% 的严格模式代码库20% 的宽松模式代码库中触发。该调查不久后被放弃。

    粒度粗糙

    冻结 API 让开发者承担了沉重的责任,必须知道应该冻结哪些原型才能维护安全的代码库,这假设开发者是安全专家。这些 API 描述了安全的_内容_,但没有描述_方式_。冻结 Object 绝对不够,因为许多利用都滥用 ArrayErrorDateReflectProxy 呢?或者未来的内置类型?冻结 API 没有为这些问题提供答案。

    冻结点

    冻结 API 假设一个稳定的冻结点:运行时中一个固定的时刻,此时原型已经稳定并且可以被冻结。实际上,这个点在积极开发的代码库中是易变的,并且随时间变化。虽然如今在许多应用中都可以找到这样一个点,但新依赖项、polyfill、代码结构更改以及热替换和开发者工具等强大功能的添加使得冻结点成为一个移动的目标。

    应用类型

    冻结 API 无法保护完整的原型链。在 JS 中,对象可以在任何时间点被添加到原型链或从中移除。要保护整个链,应该始终记住冻结添加到链中的对象,这是一个容易出错的过程。当它们从链中移除时,就不能再解冻了。

    建议的解决方案

    简而言之:一个仅通过反射 API 暴露原型的特性。如果原型不通过诸如 __proto__prototype 之类的属性可用,它们就不会暴露给仅数据问题。

    通过一个例子可以更好地理解:语句 obj[one][two] = value 容易受到通过 obj.__proto__.polluted 的原型污染攻击。如果删除 Object.prototype.__proto__ 属性,同样的语句就不再脆弱,因为它无法利用唯一另一种到达原型的方式,即 obj.constructor.prototype.polluted。请注意,原型本身不能被删除。

    本提案可以通过提供反射 API 并创建一个新的选择加入的封装特性来实现,该特性删除原型属性。每个步骤的描述如下。

    提供反射 API

    __proto__ 是一个可以被删除的遗留属性名,但它背后的内部插槽仍然可以通过 Object/Reflect.getPrototypeOf 读取,并通过 Object/Reflect.setPrototypeOf 写入,这将继续使该属性对已经运行的代码可访问。

    我们提议为 prototype 创建新的 API,例如 getClassPrototypeOfsetClassPrototypeOf,这将允许在不以任何方式改变此特殊属性工作方式的情况下删除该属性名,并支持虚拟机。

    反射 API 可以通过 polyfill 实现,这使得加固的代码库能够在所有浏览器(包括旧版本)中工作。

    选择加入的功能

    一个新颖的选择加入的“封装特性”,其中不为原型插槽的 getter 和 setter 函数创建任何属性名,这是可能的,因为对这些属性的引用可以改用反射 API。

    该功能通过一个带外标志启用:

    • 浏览器环境中,通过 HTTP 头,如 X-Encapsulate-Prototype: true
    • 其他环境中,通过功能标志,如 --encapsulate-prototype

    当封装_禁用_时,原型可通过属性和反射 API 两者访问。

    当封装_启用_时,原型仅可通过反射 API 访问,并且已删除 __proto__prototype

    封装还包含以下自动重构功能:

    自动重构

    当封装启用时,加载新源代码的 JS 引擎在其解析阶段启用一个额外步骤,该步骤将点符号访问原型属性的所有用法注册为对相应反射 API 的调用。此步骤可以高效实现,并允许具有第三方、传递或动态加载依赖项的代码库与封装兼容。

    将来,此更改将为将 prototype 标记为已弃用铺平道路。

    删除意味着什么?

    当封装启用时,原型属性可能简单地是 undefined,但当有尝试读取/写入它们时,它们可能会抛出错误。这将意味着更快地失败并大声失败,并允许测试到反射 API/封装的迁移。

    这意味着通过使用宿主钩子,使 __proto__prototype 的 getter 和 setter 有条件地基于封装,就像 eval 函数在内容安全策略下抛出异常一样。

    不兼容的代码库

    依赖_计算属性访问_来引用原型的代码与封装或自动重构不兼容。它必须被重构为在使用原型时明确引用它们。这种重构实际上使代码表达了意图,使危险模式对静态分析可见。实际上,具有此特征的代码库通常是反射框架、调试工具和其他重度反射用例,这些用例很可能知道它们如何使用原型。

    使用 prototype 一词定义自定义属性的代码库不兼容。如果此属性始终通过括号表示法设置/获取,则此类代码库可以变得兼容。根据历史 HTTP Archive 查询,有一小部分代码库因此原因不兼容。

    附录

    构造函数污染怎么办?

    constructor 属性的一些更改也可能产生超距作用。在我们的研究中,我们没有发现受此影响的实际漏洞。

    这种攻击成功的门槛极高:与原型污染一样,必须找到一个具有读写任意属性的小工具的应用。但在构造函数污染中,读取小工具必须从 constructor.polluted 读取,而不是 polluted。这大大减少了有用小工具的数量。

    最小化 JS 中的计算访问

    一些最小化 JS 可能与封装模式不兼容,因为静态属性访问可能被压缩为计算访问。我们查询了 HTTP Archive 以了解实际情况。下表显示,在过去的 12 个月中,使用桌面浏览器爬取的所有页面中,具有此行为的页面始终低于 1%:

    表格动态访问 __proto__constructor 的文档数爬取文档总数比率
    2023_03_01_desktop5,407,936609,469,4580.89%
    2023_02_01_desktop4,842,383549,089,7080.88%
    2023_01_01_desktop5,283,826589,519,1600.90%
    2022_12_01_desktop5,161,471577,073,8830.89%
    2022_11_01_desktop5,023,169561,726,2390.89%
    2022_10_01_desktop4,393,377476,880,6240.92%
    2022_09_01_desktop4,239,257466,278,7620.91%
    2022_08_01_desktop4,259,814463,784,0470.92%
    2022_07_01_desktop3,011,137339,468,6150.89%
    2022_06_01_desktop2,301,317257,501,2220.89%
    2022_04_01_desktop2,368,577263,144,6570.90%
    2022_03_01_desktop2,319,518259,249,0130.89%

    漏洞示例

    谷歌观察到提交到其漏洞奖励计划中的漏洞数量呈上升趋势:2020 年 1 个,2021 年 3 个,2022 年至今 5 个。我们在内部研究中还发现了更多。

    漏洞示例包括:

    1. 在 Web 上:应该受到 Strict CSP 保护的服务中的多个 XSS 问题。以及广泛已知的易受攻击的库
    2. 在桌面上:谷歌拥有的桌面应用中的一个 bug,用户可能被给予一个恶意 JSON 对象,由于污染漏洞,该对象可能导致本地文件泄露。(目前非公开,披露待定。)
    3. 在安全特性中:消毒器中的多个绕过,包括 Chrome 的 Sanitizer APIDOMPurify 和 Closure 消毒器
    4. 在浏览器中:一个 Firefox 沙箱逃逸 导致远程代码执行。
    5. 在 NodeJS 中:已发现多个 RCE

    我们预计,随着 JavaScript 应用部署到更多环境(例如 Electron、Cloudflare Workers 等),易受攻击的应用数量将会增长。因此,需要语言级解决方案来缓解所有环境中的攻击。