React2Shellの内部:未ドキュメントのプロトコルへの深掘りがどのようにRCEへと繋がったか

重大な脆弱性の発見は、決して直線的なプロセスではありません。多くの場合、それは執拗な好奇心、未ドキュメントの内部仕様への飛び込む意欲、そして、失敗と突破口のサイクルを突き進む粘り強さの結果です。これは、ReactとNext.jsを使用している数百万のウェブサイトに影響を与えるリモートコード実行(RCE)の脆弱性、「React2Shell」(CVE-2025-55182)において、まさにその通りでした。

React2Shellは、Reactにおけるバグの標的を絞った探索の結果ではなく、むしろ「Flight」プロトコル、つまりReactがReact Server Components (RSC)やServer Functionsを促進するために使用するメカニズムを理解しようとする探求の結果でした。このプロトコルを解体することで、セキュリティ研究者が、単純なプロパティアクセスから完全なシステム侵害へと至る経路を明らかにしました。

Flightプロトコルの理解

クライアントサイドとサーバーサイドのJavaScriptのシームレスな相互作用を可能にするために、Reactチームは「Flight」と呼ばれるプロトコルを開発しました。標準的なJSONとは異なり、FlightはDateBigIntMapSetなどの複雑なデータ型、さらには循環参照や非同期のPromisesを転送することを可能にします。

Flightメッセージは「チャンク」に分割され、型を明示するために特定の構文(例:日付には$D、参照には$x)を使用します。しかし、研究者やコミュニティメンバーが指摘したように、このプロトコルは本質的に未ドキュメントでした。この仕様の欠如は、ほとんどのセキュリティ監査人がFlightペイロードを単純なJSONとして扱い、重大な攻撃対象領域(attack surface)を見落とすことを意味していました。

最初の亀裂:プロトタイプ汚染と型強制

RCEへの道のりは、単純な観察から始まりました。Flightは、オブジェクトのプロパティ、そのプロトタイプにあるものも含めて参照することを許可していました。Number.prototype.toStringを参照するペイロードを送信することで、攻撃者はネイティブのJavaScript関数を攻撃者が制御可能なオブジェクト上に配置することができました。

これは、一見すると些細な見落としのように思われました(Vercelの創設者Guillermo Rauchは「安全性のチェックの明白な欠落」と表現しています)。しかし、これは型強制(type coercion)攻撃への扉を開きました。多くのNext.jsアプリケーションにおいて、開発者はServer Functionsのために期待される型を定義するためにTypeScriptを使用しています。しかし、TypeScriptはビルド時の分析のみを提供し、実行時の型を強制することはできません。

もし開発者が文字列を期待しているにもかかわらず、悪意のあるtoStringメソッドを持つFlightによって構築されたオブジェクトを受け取った場合、サーバーは文字列の連結中にそのメソッドをimplicitly(暗黙的)に呼び出す可能性があり、予期期せぬコード実行パスへと繋がります。

突破口:"thenables"の悪用

完全なRCEを実現するには、制御可能な引数で関数を実行する方法が必要でした。突破口は、JavaScriptがasync/awaitと「thenables」をどのように扱うかを理解することから得られました。

JavaScriptにおいて、.then()メソッドを持つあらゆるオブジェクトは「thenable」と見なされます。awaitキーワードがthenableに対して使用されるとき、JavaScriptエンジンは自動的に.then()メソッドを呼び出します。極めて重要なのは、thenableが別のthenableに解決される場合、エンジンはnon-thenableな値に到達するまで再帰的に.then()を呼び出し続けることです。

thenプロパティをArray.prototype.pushのような関数にマッピングするFlightペイロードを構築することで、研究者は、Reactの内部におけるawait decodeReply(...)が、攻撃者によって提供された関数を自動的にトリガーするようになることを発見しました。これにより、関数を暗黙的に呼び出すためのプリミティブ(primitive)が提供されました。

RCEへのエスカレーション

RCEへの最終的な飛躍は、Reactの内部的なChunkオブジェクトへのアクセスをきました。$@x構文を使用することで、攻撃者はチャンクのPromiseを生成することができます。Chunk.prototype.thenを参照することで、研究者は、React自身が攻撃者制御下のオブジェクトに対して自身の内部的な.thenの実装を強制的に実行させることができました。

これにより、研究者はChunkの内部状態、例えば「サーバーマニフェスト(server manifest)」、つまりReactに特定のServer Function IDに対してどのモジュールと関数を実行すべきかを指示するマップをSpoof(偽装)することができました。当初、研究者はNode.jsのchild_process.execをIDにマッピングッピングングする試みを行いましたが、Webpack/Turbopackがモジュールローディングをどのように扱うかという制限に直面しました。

最終的かつエレガントなエクスプロイト・チェーンは、以下の通りです:

  1. Chunkの内部を利用して、制御可能な引数で関数呼び出しをトリガーガーする。
  2. $Bxコード(ファイルblob参照に使用される)を利用して、制御可能な引数で_formData.get(...)を呼び出す。
  3. 悪意のある関数を構築し、最終的な実行をトリガーするために、最終的なthenプロパティにそれを配置する。

公開とその後

PoC(概念実証)が開発された後、再現性は極めて単純でした。新しいNext.jsアプリをインストールし、スクリプトを実行するだけで、RCEが達成されました。Metaは17時間以内に脆弱性のトリアージと確認を行い、VercelやCloudflareのようなWAFプロバイダーと連携して、公開前に防御策を実装しました。

学習した教訓訓

React2Shellの物語は、いくつかの重要なセキュリティ上の教訓を強調しています:

  • 未ドキュメントのプロトコルの危険性: フレームワークが複雑な型を扱うためにカスタム通信プロトコルを導入する場合、それは標準的なツールでは監査できない新しい攻撃対象領域を生み出します。
  • 型安全性の錯覚: TypeScriptのビルド時のチェックは、実行時のバリデーションの代わりにはなりません。信頼できない入力に対して、型注釈があるからといって変数が文字列であると仮定することは、危険な仮定です。
  • 現代的なフレームワークの複雑性: フレームワークが開発者体験を向上させるためにクライアントとサーバーのコードの境界を曖昧にすることで、内部の「配管(plumbing)」の複雑さが増大し、しばしば、深い、体系的な分析を通じてのみ発見可能な、微妙な脆弱性が導入される原因となります。

あるコミュニティメンバーが指摘したように、この脆弱性は、より大きなアーキテクチャ上のリスクの兆候でした:「クライアントコードとサーバーコードの境界を曖昧にすること...そして、プロトコルがプリミティブだけでなくコードをシリアライズすることを許可してしまうこと」です。

Sources