📝

React2Shell(React/RSC/CVE-2025-55182)について、パッチPRを読んでまとめてみた

に公開

はじめに

12月3日にパッチと共に発表されたReact Server Components (RSC)の脆弱性情報React2Shell (CVE-2025-55182) が発表されました。ふーんと思ってみてみると、CVSS (Common Vulnerability Scoring System)スコア10.0!?

そもそもReactにもまだまだ詳しいわけではないですが、内容が気になってしまったのでReactの勉強がてらパッチを読んで自分なりにまとめてみました。

このかなり緊急性の高い脆弱性が具体的にどのような技術的欠陥によって引き起こされていたのか、修正前のコードと修正後のコードを比較しながらまとめていこうと思います。


1. ざっくり要約

この脆弱性はざっくりいうと、JavaScriptのデシリアライズ処理におけるプロトタイプチェーンのアクセス制御の欠如、特にObject.prototypeへの不正なアクセスと書き込み(プロトタイプ汚染)が可能になってしまっていたという部分にあります。

脆弱性から繋がる影響

クライアントからサーバーに送信されたデータが、十分にアクセス制御がなされていない方法で処理されることにより、攻撃者は以下のことが可能になりました。

  1. プロトタイプ汚染: Object.prototypeに任意のプロパティを設定し、すべてのオブジェクトに影響を与える。
  2. 認証・権限のバイパス: 汚染されたプロパティを利用して、認証バイパスや権限昇格を行う。
  3. リモートコード実行 (RCE): 最終的に、システムの内部ロジックを悪用し、サーバー上で任意のコード実行(RCE)に繋がる可能性があった。

根本原因となったファイル

この問題は、RSCでクライアントから送信されたデータをデシリアライズする処理が実装されている、ReactFlightReplyServer.js 内の処理に起因していました。(パッチPRでもこの部分でクリティカルな修正を行なっていると明言されています。) PR上のコメント


2. 脆弱性が存在した3つの関数

ReactFlightReplyServer.js内の以下の3つの関数で、オブジェクト自身が持つプロパティだけでなく、継承元(プロトタイプチェーン)のプロパティにもアクセスできるようになってしまっていました。

2.1. reviveModel() (JSONオブジェクトの処理)

RSC Payloadから送られてきたJSON文字列をパースしてJSONオブジェクトにした後に呼ばれ、そのオブジェクトのプロパティを再帰的に処理する際に使用される関数です。

仮に、パースされたJSONオブジェクトが
{ onClick: "$F5", data: "$@3", items: ["apple", "$@7"] }のような内容だった場合の処理の流れは、

  1. 特殊なフォーマット($F=関数, $@=Promise等)ならそれぞれに応じた形(通常の関数やPromise)に変換、普通の文字列ならそのまま
  2. 文字列のモジュール参照(例: ""5:bound:args"")が見つかった場合には、後述のgetOutlinedModel()を実行する
  3. オブジェクト/配列なら、上の処理を各要素に対して再帰的に行う
  4. 3までで変換した値で、元のオブジェクトの該当キーの値を上書き
    変換結果の例:
    { onClick: function(){...}, data: Promise, items: ["apple", Promise] }

修正前の問題点

// 修正前コード(該当箇所)
for (const key in value) {
  // ここで継承元のプロパティにもアクセスできるようになってしまっていた
  value[key] = newValue;
}
// 省略

for...inループ内で、オブジェクトが継承したプロパティも列挙の対象となり、そのプロパティに対して値が代入されてしまう可能性がありました。これが、直接的にプロトタイプ汚染を引き起こす経路となっていました。

2.2. getOutlinedModel() (同期的なモジュール参照解決)

この関数は、チャンクデータ[1]がすぐに利用可能な場合に、クライアントから送られた文字列のモジュール参照(例: "5:bound:args")を同期的に解決する役割を持ちます。

修正前の問題点

仮に、この関数に送られた参照文字列が"5:bound:args"だった場合、

  1. 参照をパースしてチャンクID取得 ("5:bound:args" → id=5, path=["bound","args"])
  2. getChunk(5)でチャンクを取得 → 該当のチャンクのステータスがまだPENDINGならwaitForReference()で待機(その後後述のfulfillReference()が呼ばれる)
  3. チャンクがINITIALIZEDなら、pathを辿る (chunk.value.bound.args)

修正前の処理で、仮に参照文字列が"$F0:constructor:prototype"のような形式だった場合、3の段階でObject.prototypeにアクセスすることが可能になってしまいます。

// 修正前コード(該当箇所)
for (let i = 1; i < path.length; i++) {
  value = value[path[i]];  // 
}
// 省略

上記の箇所において、hasOwnProperty()によるチェックがなかったため、constructor や prototype といった継承プロパティにもアクセス可能でした。これにより、攻撃者はプロトタイプチェーンを辿り、Object.prototype への参照を取得することができてしまいます。

2.3. fulfillReference() (非同期的なモジュール参照解決)

getOutlinedModel()でチャンクデータがなんらかの理由でまだ利用可能ではない場合に、データ取得完了後に非同期で参照を解決する関数です。

修正前の問題点

処理はgetOutlinedModel()とほぼ同じで、同様に継承元のプロパティにアクセスできるようになっており、それを辿ってObject.prototypeの参照を返してしまう可能性がありました。

3. 修正内容:hasOwnPropertyの導入

これらの問題に対応するため、修正PRでは、いずれの関数においてもオブジェクト自身が持つプロパティのみを処理するためのhasOwnProperty.call(value, key)チェックが追加されました。

3.1. hasOwnProperty.call(obj, key)

hasOwnProperty.call(obj, key)は、第一引数に対象のオブジェクトと、第二引数にプロパティ名を受け取ります。

第一引数として受け取ったオブジェクトobjが、第二引数として受け取ったkeyのプロパティを持っているかどうかを判定して、持っていればtrue、そうでなければfalseを返します。

この時、継承元のプロトタイプには遡らないので、プロトタイプに対する処理の実行そのものを防ぐことができます。

getOutlinedModel / fulfillReference 関数の修正

// 修正後コード(該当箇所)
const name = path[i];
// hasOwnPropertyチェックの追加
if (typeof value === 'object' && hasOwnProperty.call(value, name)) { 
  value = value[name];  // オブジェクト自身が持つプロパティのみアクセス
}
// ...

reviveModel 関数の修正

// 修正後コード(該当箇所)
for (const key in value) {
  // hasOwnPropertyチェックの追加
  if (hasOwnProperty.call(value, key)) { 
    // ...
    const newValue = reviveModel(
      // ...
    );
    if (newValue !== undefined || key === '__proto__') {
      // ...
      value[key] = newValue;
    } else {
      // ...
      delete value[key];
    }
  }
}

修正による効果

この修正により、以下のことが実現されました。

  • 継承プロパティへのアクセスを防止: constructorやprototypeなどの継承プロパティを、クライアントからの入力によって不正に辿ることができなくなりました。
  • プロトタイプ汚染の防止: Object.prototypeへの不正な書き込みが制御され、サーバー全体のオブジェクトが汚染されるリスクがなくなりました。

最後に

今回の脆弱性は、デシリアライズ処理における基本的なプロトタイプチェーンのアクセス制御の欠如が原因のひとつとなったものでした。

この調査を通じてReact内部の処理について調べるきっかけになり、さらに気になる部分もポツポツと出てきたので今後も色々調べていきたいと思います!

記事の内容について、筆者の理解不足や抜け漏れがありましたら、遠慮なく教えていただけると嬉しいです。

参照情報

脚注
  1. RSC Payloadはクライアントとサーバーを行き来する際、分割して処理するような仕組みになっています。これにより、大規模なアプリケ-ションであったとしても、部分的に素早くインタラクティブになり、体験を損なわないようにすることができます ↩︎

Discussion