Curtailing the power of "Thenables" S2
中文标题:限制“Thenable”的能力(SafeResolve)
- 阶段: Stage 2
- 状态: 进行中
- ECMAScript 版本: —
- 同步时间: 2026年8月26日
- English original · 官方仓库
该提案解决了因原型链上then查找而使用“thenables”解析Promise时带来的安全性和复杂性风险。它引入了一个“SafeResolve”操作,在解析前检查是否可以运行用户代码,如果可以,则排队一个任务来解析并锁定Promise。
以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。
限制“Thenable”的能力(SafeResolve)
提案发起人: Matthew Gaudet (Mozilla)
阶段: 2(截至2026年3月全体会议)
规范草稿文本: https://tc39.es/proposal-thenable-curtailment/
引言与问题:
引用MDN:
JavaScript生态在它成为语言的一部分之前就已经有了多个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能力的[[Resolve]]操作。如果可能运行用户代码,我们则改为将一个新任务排队,该任务负责解析Promise,同时像常规resolve函数那样锁定Promise,以便未来的解析被忽略(非常感谢Mark Miller在TG3审查讨论中抓住了这一要求)。
下一步是决定如何使用它。Mozilla DOM方面有兴趣探索使用它来替换WebIDL中的Promise解析步骤,并更广泛地为Mozilla DOM中的所有Promise解析代码提供支持。这将使C++代码更安全,因为Promise解析成为一种永不运行脚本的操作,简化了实现代码时所需的推理。
关于WebIDL消耗的规范讨论正在whatwg/webidl #1584进行。
将此暴露给用户代码不是本提案的目标,但可以作为后续提案实现。
这是万无一失的修复吗?
不是。其影响当然完全取决于采用的范围。
在之前描述的安全漏洞中,此缓解措施将修复:
- ReadableStream::Close 中的越界访问
- 一些未公开的漏洞。
- CVE-2024-9086,因为在那个漏洞中,只需将“then”属性添加到一个已逃逸到脚本的实例即可。
- 我怀疑Chrome动画的释放后使用,但不确定。
但它无法修复:
- CVE-2024-43357 针对规范。那需要规范内部的规范性变更才能开始使用这个能力。
兼容性
这可能会改变涉及“Thenable”时微任务的解析顺序。希望大多数处理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来洞察执行顺序。该测试不再测试它认为自己测试的内容;然而,该测试最初也是为了解决这类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的'然后'使用不值得减慢所有Promise操作”- Proposal Stabilize 试图提供可泛化的不变量机制——这可能会成为我们也能提供给用户代码的不变量。
提案历史
- 在2025年2月全体会议上展示 -- 达到阶段1。(笔记)
- 在2025年7月全体会议上展示。(笔记 和 续篇笔记)
- 在2026年3月全体会议上展示
- 在2026年5月全体会议上展示 (笔记待发布)
Footnotes
-
这比严格做WebIDL稍微广泛一些,因为我认为存在非IDL对dom::Promise的使用。 ↩