Skip to content

tc39/proposal-thenable-curtailment

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

41 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Curtailing the power of "Thenables" (SafeResolve)

Champion: Matthew Gaudet (Mozilla)

Stage: 2 (As of March 2026 Plenary)

Draft Spec Text: https://tc39.es/proposal-thenable-curtailment/

Introduction & Problem:

Quoting MDN:

The JavaScript ecosystem had made multiple Promise implementations long before it became part of the language. Despite being represented differently internally, at the minimum, all Promise-like objects implement the Thenable interface. A thenable implements the .then() method, which is called with two callbacks: one for when the promise is fulfilled, one for when it's rejected. Promises are thenables as well.

To interoperate with the existing Promise implementations, the language allows using thenables in place of promises. For example, Promise.resolve will not only resolve promises, but also trace thenables.

The problem we would like to address is that then lookup follows the whole prototype chain. Including builtin prototypes and Object.prototype. This is particularly dangerous when working with types where 'thenable' dispatch was unexpected.

Why is this a problem?

The most concrete one is security vulnerabilities. We must be ever-vigilant about this compatibility supporting feature in all standard work, and throughout the web-platform. Failure to do so has the consequence of possible exploitation:

The reason this particular issue is fingered for causing security vulnerabilities is that it adds many paths for user code execution which otherwise don't exist, and is not always obviously a possibility.

Of particular danger is where specification authors think of newborn objects of known types as known quantities, only to call Promise.resolve on them. At this point when they are provided a JS wrapper the JS wrapper typically has Object as their prototype, making them vulnerable to thenables.

Beyond security, this also just injects complexity. There are test cases in WPT that exist purely to work out the expected behaviour for someone breaking then

How do I propose we fix this?

I'd like to propose we add a "SafeResolve" resolve operation, which resolves a promise after checking for the possibility of running user code. If we cannot run any user code, we simply tail-call into the promise capability's [[Resolve]] operation. If we could run user-code, we instead enqueue a new job whose responsibility is to resolve the promise while also latching the promise in the same way the regular resolve functions do, such that any future resolutions are ignored (Thank you very much to Mark Miller for catching this requirement in TG3 review discussion).

The next step is to decide how to consume this. There is interest from the Mozilla DOM to explore using this to replace the steps for resolving a promise in WebIDL and more generally powering all the promise resolution code in Mozilla's DOM. This would help make C++ code safer by making promise resolution into an operation that never runs script, which simplifies the reasoning required when implementing code.

Specification discussion about WebIDL consumption is happening at whatwg/webidl #1584

Exposing this to user-code is a non-goal of this specific proposal, but can be done as a followup proposal eventually.

Is this a bulletproof fix?

No. The impact of this will of course depend entirely on the scope of adoption.

Of the previously described security bugs this mitigation would fix

It would not however fix

  • CVE-2024-43357 on the specification. That would require a normative change to start consuming this capability within the specification.

Compatibility

This could change the order in which microtasks get resolved when 'thenables' are involved. The hope is that the majority of code dealing with promises is already relatively robust to execution order. However, it is certainly plausible this could cause a web compatibility problem.

Experiment: WebIDL

Q: Can we use a SafePromiseResolve to replace the promise resolution steps in WebIDL?

Experiment: Run WPT with the Firefox DOM Promise resolve steps replaced with SafePromiseResolve1.

Results:

The vast majority of tests (as expected) pass.

Timeout Failures:

  1. https://searchfox.org/firefox-main/source/testing/web-platform/tests/css/css-overflow/scroll-marker-in-display-none-column-crash.html -- I didn't quite figure this one out.
  2. /custom-elements/when-defined-reentry-crash.html -- this one is using a then on Object.prototype for nefarious aims. In a sense this is exactly the kind of issue we’re trying to address. https://issues.chromium.org/issues/40061097

Unexpected Pass:

  1. /fetch/api/response/response-body-read-task-handling.html - This test is using then to get insight into execution order. The test no longer tests what it thinks it is testing anymore; however the test -also- was created to address this kind of thenable issue.

Test Failures

  1. /streams/readable-byte-streams/patched-global.any.js -- Explicitly using then to peek into execution state we’d probably prefer to not be observable.
  2. /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" -- The job timing changes because it’s resolving a promise with a window (WindowProxy) object, which causes an extra tick.
  3. /web-animations/interfaces/Animation/cancel.html; observing event timing with thenable.

Prior Art & Related Work

  • Symbol.thenable "Withdrawn; changing thenability on Module Namespace objects is not web compatible, and allowing non-Promise use of "then" is not worth slowing down all Promise operations"
  • Proposal Stabilize is trying to provide generalizable machineries for invariants -- this could be more of an invariant we could provide to user code as well.

Proposal History

Footnotes

  1. This is slightly more broad than strictly doing WebIDL because I think there’s non IDL use of dom::Promise.

About

A proposal to curtail the power of "thenable" objects.

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Contributors