Curtailing the power of "Thenables" S2
中文标题:限制“Thenables”的能力(SafeResolve)
- 阶段: Stage 2
- 状态: 进行中
- ECMAScript 版本: —
- 同步时间: 2026年9月19日
- English original · 官方仓库
该提案针对 thenable 的 then 查找沿完整原型链进行所引发的风险与复杂性,这种查找可能在 promise 解析期间意外执行用户代码,造成安全漏洞。它引入了一种“SafeResolve”promise 解析操作:先检查解析 promise 是否可能运行用户代码;若不可能,则直接调用 promise capability 的 \[\[Resolve\]\];若可能,则改为入队一个任务,以锁定语义解析 promise,从而忽略后续解析。该操作预期用于内部场景(例如 WebIDL 的 promise 解析),向用户代码暴露它不是本提案的目标。
以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。
限制“Thenables”的能力(SafeResolve)
Champion(提案负责人):Matthew Gaudet(Mozilla)
阶段:2.7(截至 2026 年 7 月全体会议)
规范草案文本:https://tc39.es/proposal-thenable-curtailment/
引言与问题:
引用 MDN:
JavaScript 生态系统在 Promise 成为语言的一部分之前就已经有了多种 Promise 实现。尽管这些实现内部表示不同,但所有类 Promise 对象至少都实现了 Thenable 接口。Thenable 实现了
.then()方法,该方法接收两个回调:一个用于 promise 兑现时,一个用于 promise 拒绝时。Promise 本身也是 thenable。为了与现有的 Promise 实现互操作,语言允许使用 thenable 代替 promise。例如,Promise.resolve 不仅会解析 promise,还会追踪 thenable。
我们要解决的问题是:then 的查找会沿着整个原型链进行,包括内置原型和 Object.prototype。在处理那些“thenable 派发”并不预期的类型时,这尤其危险。
为什么这是一个问题?
最具体的后果是安全漏洞。我们必须始终对这一兼容性支持特性保持高度警惕,在所有标准工作和整个 Web 平台中都是如此。否则可能导致被利用:
- CVE-2024-43357 规范漏洞。
- ReadableStream::Close 中的越界访问
- CVE-2021-21206:Chrome 动画中的释放后使用
- CVE-2024-9086
- 其他未在此披露的问题。
这一特定问题被指为导致安全漏洞的原因是:它为用户代码执行增加了许多原本不存在的路径,而且并不总是很明显地存在这种可能性。
尤其危险的是,规范作者将已知类型的新生对象视为已知量,却对它们调用 Promise.resolve。在这种情况下,当提供给它们一个 JS 包装对象时,该 JS 包装对象的原型通常是 Object,从而使它们易受 thenable 攻击。
除了安全之外,这也只是引入了复杂性。WPT 中有一些测试用例完全是为了搞清有人破坏 then 时的预期行为。
我提议如何修复?
我想提议添加一个“SafeResolve”解析操作,它在检查是否可能运行用户代码之后解析 promise。如果我们无法运行任何用户代码,则直接尾调用 promise capability 的 [[Resolve]] 操作。如果可能运行用户代码,我们则改为将一个新任务入队,该任务负责解析 promise,同时以与常规 resolve 函数相同的方式锁定该 promise,使得任何未来的解析都会被忽略(非常感谢 Mark Miller 在 TG3 审查讨论中发现了这一要求)。
下一步是决定如何使用它。Mozilla DOM 方面有兴趣探索使用它来替代 WebIDL 中解析 promise 的步骤,并更广泛地支持 Mozilla DOM 中所有 promise 解析代码。这将使 promise 解析成为永不运行脚本的操作,从而使 C++ 代码更安全,简化实现代码时所需的推理。
关于 WebIDL 使用的规范讨论正在进行中,见 whatwg/webidl #1584
将此功能暴露给用户代码不是本提案的目标,但最终可以作为后续提案完成。
这是万无一失的修复吗?
不是。其影响当然完全取决于采用范围。
在之前描述的安全漏洞中,此缓解措施能修复:
- ReadableStream::Close 中的越界访问
- 一些未披露的漏洞。
- CVE-2024-9086,因为在该漏洞中,只需将“then”属性添加到已逃逸到脚本的实例上就足够了。
- 我怀疑 Chrome 动画 UAF 也能修复,但不太确定。
然而它不会修复:
- CVE-2024-43357 规范漏洞。那需要进行一项规范性修改,以开始在规范_内部_消费这一能力。
兼容性
这可能会改变涉及“thenables”时微任务的解析顺序。希望大多数处理 promise 的代码对执行顺序已经相对健壮。然而,这确实有可能导致 Web 兼容性问题。
实验:WebIDL
问:我们能否用 SafePromiseResolve 替换 WebIDL 中的 promise 解析步骤?
实验:使用 SafePromiseResolve 替换 Firefox DOM 的 promise 解析步骤来运行 WPT1。
结果:
绝大多数测试(如预期)通过。
超时失败:
- https://searchfox.org/firefox-main/source/testing/web-platform/tests/css/css-overflow/scroll-marker-in-display-none-column-crash.html —— 我还没完全搞清楚这个问题。
- /custom-elements/when-defined-reentry-crash.html -- 这个测试在
Object.prototype上使用then以达到不良目的。从某种意义上说,这正是我们要解决的问题。https://issues.chromium.org/issues/40061097
意外的通过:
- /fetch/api/response/response-body-read-task-handling.html —— 这个测试使用
then来窥探执行顺序。该测试不再测试它原本以为在测试的内容了;不过,该测试 -also- 是为了解决这类 thenable 问题而创建的。
测试失败
- /streams/readable-byte-streams/patched-global.any.js —— 明确使用
then来窥探我们可能更希望不可观察的执行状态。 /document-picture-in-picture/returns-window-with-document.https.html | requestWindow timing - assert\_equals: Got the expected order of actions expected "requestWindow,microtask,enter" but got "microtask,requestWindow,enter"-- 任务时机改变,因为它用一个window(WindowProxy)对象解析 promise,导致多了一个 tick。- /web-animations/interfaces/Animation/cancel.html;使用 thenable 观察事件时序。
先前工作与相关工作
Symbol.thenable“已撤回;在模块命名空间对象上更改 thenability 不兼容 Web,并且允许非 Promise 使用“then”不值得减慢所有 Promise 操作”- Proposal Stabilize 试图为不变式提供可推广的机制——这或许也可以成为我们能够提供给用户代码的一种不变式。
提案历史
- 2025 年 2 月全体会议上的演示 -- 达到 Stage 1。(记录)
- 2025 年 7 月全体会议上的演示。(记录 和 续篇记录)
- 2026 年 3 月全体会议上的演示
- 2026 年 5 月全体会议上的演示 (记录)
- 2026 年 7 月全体会议上的演示,达到 Stage 2.7(记录待发布)
Footnotes
-
这比严格意义上的 WebIDL 略微宽泛,因为我认为 Mozilla 的 dom::Promise 存在非 IDL 使用。 ↩