【技術詳細】React 19.0.x (CVE-2025-55182) 脆弱性修正のコードレベル解説

本記事では、React Server Components (RSC) における深刻な脆弱性(CVE-2025-55182)に対し、React 19.0.1 で行われた具体的な修正内容をソースコードレベルで解説します。

主な修正方針は、Flightプロトコル(RSC通信)のデシリアライズ処理における hasOwnProperty チェックの徹底と、参照解決フローの厳格化によるプロトタイプ汚染およびRCE(リモートコード実行)の防止です。

1. サーバ側:参照解決ロジックの刷新

概要: createModelResolver を廃止し、より堅牢な fulfillReference を導入しました。

  • 修正前ファイル(該当コミット時点):
    packages/react-server/src/ReactFlightReplyServer.js

Before: 修正前(脆弱性が存在したロジック)

createModelResolver 関数では、検証を行わずにパスを辿り、そのまま親オブジェクトへ代入していました。

function createModelResolver<T>(
  chunk: SomeChunk<T>,            // 対象のチャンク(非同期で解決される単位)
  parentObject: Object,           // 結果を割り当てる親オブジェクト
  key: string,                    // 割り当て先のキー名
  cyclic: boolean,                // 循環参照の可能性があるか
  response: Response,
  map: (response: Response, model: any) => T,
  path: Array<string>,            // 参照パス("id:prop:sub" のような構造)
): (value: any) => void {
  let blocked;
  // ...(初期化処理省略)...

  return value => {
    // 🚨 問題点: 単純に path に沿ってドリルダウンしている
    for (let i = 1; i < path.length; i++) {
      value = value[path[i]];
    }
    // 🚨 問題点: 検証なしにそのまま代入している
    parentObject[key] = map(response, value);

    // ...(後続処理省略)...
  };
}

【問題点】

  • value = value[path[i]] の処理において hasOwnProperty による検査がないため、攻撃者が __proto__constructor をパスに含めることでプロトタイプ上のプロパティにアクセス可能です。
  • 受信データを検証なしに parentObject[key] に書き込むため、プロトタイプ汚染の影響が確定してしまいます。

After: 修正後(対策済み)

  • 修正後ファイル(v19.0.1):
    packages/react-server/src/ReactFlightReplyServer.js

fulfillReference 関数を導入し、ReactPromise(チャンク)の状態確認と hasOwnProperty によるガードを追加しました。

function fulfillReference(
  response: Response,
  reference: InitializationReference, // 参照(ハンドラ・親・キー・マップ関数・パスを含む)
  value: any,
): void {
  const {handler, parentObject, key, map, path} = reference;

  for (let i = 1; i < path.length; i++) {
    // 🛡️ 対策1: value がチャンクなら状態を確認し、未解決なら待機リストへ
    while (value instanceof ReactPromise) {
      const referencedChunk: SomeChunk<any> = value;
      switch (referencedChunk.status) {
        case RESOLVED_MODEL:
          initializeModelChunk(referencedChunk);
          break;
      }
      switch (referencedChunk.status) {
        case INITIALIZED: {
          value = referencedChunk.value;
          continue;
        }
        case BLOCKED:
        case PENDING: {
          // 未解決の場合はパスを調整し、待ちリストに登録して処理を中断(return)
          path.splice(0, i - 1);
          if (referencedChunk.value === null) {
            referencedChunk.value = [reference];
          } else {
            referencedChunk.value.push(reference);
          }
          // ...(reasonの処理省略)...
          return; // 即座に代入せず、解決を待つ
        }
        default: {
          rejectReference(response, reference.handler, referencedChunk.reason);
          return;
        }
      }
    }

    const name = path[i];
    // 🛡️ 対策2: プロパティアクセス前に own-property をチェック
    if (typeof value === 'object' && hasOwnProperty.call(value, name)) {
      value = value[name];
    } else {
      // own-property ではない場合は読み飛ばす(プロトタイプ汚染を防ぐ)
      value = undefined;
      break;
    }
  }

  // 安全に検査済みの値を代入
  const mappedValue = map(response, value, parentObject, key);
  parentObject[key] = mappedValue;

  // ...(後続のストリーム解決処理)...
}

【変更点と効果】

  1. 安全なアンラップ: 未解決のチャンクに遭遇した場合、即座に処理を中断し、解決待ちリストに登録するフローに変更されました。
  2. hasOwnProperty ガード: value[name] にアクセスする前に必ず hasOwnProperty.call(value, name) を実行することで、プロトタイプチェーン上のプロパティへのアクセスを物理的に遮断しています。

2. サーバ側:reviveModel 内の __proto__ ガード

データの復元(Revive)処理においても、プロパティ書き込み時のチェックが強化されています。

Before vs After (抜粋比較)

Before:

// 検証なしで書き込み
if (newValue !== undefined) {
  value[key] = newValue;
}

After:

// 🛡️ __proto__ に対する特殊制御と own-property の前提
if (newValue !== undefined || key === '__proto__') {
  value[key] = newValue;
}

【変更点】

  • hasOwnProperty.call(value, key) で自身のプロパティのみを対象にするループ内での処理です。
  • newValueundefined の場合でも key__proto__ なら代入を行う条件が追加されました。これは fulfillReference 等の新しい初期化フローと整合性を取るための調整であり、プロトタイプ汚染を許容するものではありません。全体として own-property のチェックが前提となっています。

3. クライアント側バンドラ:requireModule の修正

サーバーだけでなく、クライアント側(react-server-dom-webpack 等)のモジュール読み込みにも修正が入りました。

Before

export function requireModule<T>(metadata: ClientReference<T>): T {
  const moduleExports = parcelRequire(metadata[ID]);
  // 🚨 脆弱: 直接プロパティを返している
  return moduleExports[metadata.name];
}

After

import hasOwnProperty from 'shared/hasOwnProperty';

export function requireModule<T>(metadata: ClientReference<T>): T {
  const moduleExports = parcelRequire(metadata[ID]);
  // 🛡️ 対策: own-property であることを明示的にチェック
  if (hasOwnProperty.call(moduleExports, metadata.name)) {
    return moduleExports[metadata.name];
  }
  // 存在しない/プロトタイプ由来の場合は undefined を返す
  return (undefined: any);
}

【効果】

もし moduleExports のプロトタイプが汚染されていたとしても、自身のプロパティでない限り無視されるため、攻撃者が仕込んだ偽のエクスポート関数が実行されるのを防ぎます。

4. ストリーム / エラーハンドリングの強化

multipart/busboy 等を使用するストリーム処理において、例外発生時の挙動が安全側に倒されました。

  • Before: 例外発生時にそのまま throw され、ストリームが中途半端な状態で残るリスクがありました。
  • After: resolveField / resolveFileComplete の呼び出しを try/catch で保護。例外時は busboyStream.destroy(error) を呼び出し、即座にストリームを破棄(Fail-Fast)するよう変更されました。

これにより、攻撃者が不正なストリームデータを送って部分的な初期化状態を作り出し、そこから更なる攻撃を行うリスクを低減しています。

5. まとめ:なぜこれで脆弱性が防げるのか

今回のパッチ(React 19.0.1)により、以下の防御層が確立されました。

  1. 入口封鎖 (hasOwnProperty):
    Flightプロトコルのデシリアライズ処理において、__proto__constructor といったプロトタイプチェーン上のプロパティへのアクセスを hasOwnProperty.call で徹底的に排除しました。
  2. 実行制御 (Chunk Handling):
    非同期データの解決フローを見直し、未解決データを即座に代入せず待機リストへ回すことで、不正な状態での実行を防ぎました。
  3. Fail-Fast:
    エラー発生時にストリームを即座に破棄することで、攻撃の足がかりとなる「不安定な状態」を残さないようにしました。

これらの対策により、「外部入力 → 無検査でのプロパティアクセス → プロトタイプ汚染 → RCE」という攻撃チェーンが断ち切られています。