どういうこと?
Object.prototype.then = function(resolve, reject) {
console.log('thenメソッド生やしてみた');
resolve(true);
};
async function dummy() {
await {};
}
dummy(); // "thenメソッド生やしてみた"
{}というPromiseでもなんでもないオブジェクトをawaitしただけなのに何故かthenが発動してしまいました。
なんで???
実はawait演算子は、『引数がPromiseであれば解決する』ではなく『引数がthenableであれば解決する』という仕様になっています。
それではthenableか否かはどうやって判定されるのかというと、『thenメソッドがある』です。
マジかよ。
どうしてこんな仕様なのかというと、JavaScriptの仕様にPromiseやasync/awaitがなかったころから各ブラウザに独自実装されていた大昔の挙動との互換を保つためにそうなっているということです。
しかし、こんなコードが動くとなれば、たいへん嫌な予感がしますよね。
はい、ご存じCVE-2025-55182ことReact2Shellは、このthenインジェクションを正面から活用したものでした。
// FROM trendmicro
{
0: {
status: "resolved_model",
reason: -1,
_response: {
_prefix: "console.log('RCE')//",
_formData: { get: "$1:then:constructor" },
},
then: "$1:then",
value: '{"then":"$B"}',
},
1: "$@0",
}
このソースコードの詳細は解説記事を見てもらうとして、thenメソッドに何かやってるというのは見てわかるでしょう。
これによって想定外のオブジェクトをresolveさせることが可能になり、サーバ側で任意のコードを実行するための足がかりになりました。
他にもCVE-2024-43357やCVE-2024-9680、CVE-2021-21206といった様々な脆弱性が、この仕様のせいで発生しています。
ということで、いいかげんどうにかしようというProposalが提出されました。
ただ互換性を保つためなのか、単純に『引数がPromiseであれば解決する』にはせず、thenがどこから来たのか確認してどうこうと、またなんか抜け道が見つかりそうなややこしい解決策が提示されていました。
ということで以下は該当のProposal、Curtailing the power of "Thenables" (SafeResolve)の紹介です。
現在のステージは2.7で、仕様は概ね決まっていてテスト段階です。
Curtailing the power of "Thenables" (SafeResolve)
Introduction & Problem
MDNからの引用。
JavaScript のエコシステムには、プロミスが言語の一部となるずっと前から、複数のプロミスの実装がありました。内部では様々な形で表現されていますが、最低限、すべてのプロミス風のオブジェクトは Thenable インターフェイスを実装しています。 Thenable は .then() メソッドを実装しています。これは 2 つのコールバックを取り、1 つはプロミスが履行されたとき、もう 1 つはプロミスが拒否されたときに呼び出されます。プロミスは Thenable でもあります。
既存のプロミス実装と相互運用するために、言語ではプロミスの代わりに Thenable を使用することができます。例えば、 Promise.resolve はプロミスの解決だけでなく、 Thenable の追跡も行います。
我々がどうにかしたい問題点は、thenルックアップがプロトタイプチェーン全体を辿ることです。
これにはObject.prototypeも含まれます。
これは、thenableが予期しない型を扱う場合は特に危険です。
Why is this a problem?
なにが問題?
最もわかりやすい問題はセキュリティ上の脆弱性です。
全ての標準機能やWebプラットフォームは、互換性サポートにおいてもセキュリティに気をつけなければなりません。
設計に問題があると、悪用される可能性が高まります。
・CVE-2024-43357 仕様上の問題
・ReadableStream::Close 境界外アクセス
・CVE-2021-21206 Use-After-Free脆弱性
・CVE-2024-9086
・その他開示されていない脆弱性もある
本仕様がセキュリティ上の脆弱性の原因とされる理由は、本来は存在しないはずのユーザコード実行経路を追加してしまうこと、そしてそのことに気付かれにくいことです。
特に危険なのは、開発者が既知の型の新しいオブジェクトを既知の型の安全なオブジェクトだとみなしてPromise.resolveを呼び出すケースです。
JavaScriptのオブジェクトは通常、プロトタイプがObjectになるのでthenableに対して脆弱になります。
セキュリティ上の問題だけではなく、必要以上に複雑さが増すという問題もあります。
Web Platform Testsには、thenをどうにかしたときに動きがどうなるべきかを規定するだけのテストも存在します。
How do I propose we fix this?
どうすればいい?
本proposalでは、ユーザコードが実行される可能性があるかを調べたうえでPromiseを解決するSafeResolveを導入します。
ユーザコードが実行されないのであれば、これまでどおり単にPromise.resolveを呼び出します。
ユーザコードが実行される可能性があれば、そのPromiseを解決するための新しい非同期ジョブをキューに登録し、さらに一度だけしか実行できないようにします。
MozillaのDOMチームは、WebIDLのPromise解決をこれに置き変えました。
さらにいずれは全てのPromise解決をこれに置き換えることを検討しています。
これによって、Promise解決スクリプトがC++の動作中に動かなくなるため、C++コードの安全性が高まります。
またコード実装時の考慮事項もシンプルになります。
WebIDLでの検討はwhatwg/webidl #1584で行われています。
Is this a bulletproof fix?
これは完璧な対策ですか?
いいえ。
前述のリストのうち、この対策で修正されるセキュリティバグは以下です。
・ReadableStream::Close
・CVE-2021-21206
・CVE-2024-9086
こちらは解決されません。
Compatibility
thenableが関わる場合、マイクロタスクの実行順が変わることがあります。
Promiseを扱うコードは大抵実行順について堅牢であると思われますが、互換性の問題が発生する可能性はあります。
Prior Art & Related Work
関連研究。
撤回された。
モジュール名前空間でのthenの挙動変更はWebとの互換性がなく、導入による速度低下を上回る価値はないと判断された。
汎用的な不変の仕組みを導入しようとしている。
Proposal History
・2025年2月 ステージ1になった
・2025年7月
・2026年3月
・2026年5月
・2026年7月 ステージ2.7になった
感想
正直このProposalだけだと全然よくわからなかったので追加で諸々調べる羽目になりました。
どうやら最初に上げたthenでresolveが動くこと自体はこのproposalでは特に問題視されておらず、その解決処理が現状ではブラウザ内部のC++コードと同時に動くので、結果としてUse-After-Freeやら境界外アクセスといったメモリ破壊が起きるのが問題だということみたいです。
そこでresolveは即時実行せずキューに入れてしまって、他の処理が終わった後で実行するという解決策になりました。
ということは、メモリ破壊系の脆弱性は防げたとしても、単発のプロトタイプ汚染による不正データ送信とかは防げないのでは?
このへんよくわからないので誰か教えてくだちい。