React RCE事件から学ぶ教訓:今こそHMAC署名とキー回転、ゼロトラストが必要な理由

RCE脆弱性が示したこと:"データを信じる瞬間に終わる"



最近、React Server Components/Next.jsで発生したRCE脆弱性(CVE-2025-55182)は、単に"Reactが突破された"というニュースで終わるものではありません。この事件が示した核心メッセージは、もっと原理的です。

"クライアントが送ったデータを一度でも信頼すれば、いつか必ず突破される"

今回の脆弱性の本質は、サーバーがクライアントから送られたFlightプロトコルデータ(メタデータ)を十分な検証なしにモジュールロード・オブジェクトアクセスに直ちに使用した点にあります。

  • "この値はReactが安全に処理してくれる"。
  • "私たちが直接使うAPIではないので大丈夫だろう"。

このような暗黙の信頼が蓄積され、事前認証なしでRCEが可能なポイントまで至ったのです。

今、質問を変える必要があります。

  • "Reactがなぜ突破されたのか"ではなく
  • "今、私たちはどのデータを、署名・検証なしでそのまま信じているのか"

その質問への答えを探す過程で自然に登場すべき概念が、HMAC署名ゼロトラストです。


なぜHMAC署名が重要か:"このデータは本当に私たちのサーバーが作ったものか"?

実際のシステムは、思ったよりも"サーバー間の信頼"に大きく依存しています。

  • フロントエンド ↔ バックエンド
  • マイクロサービスA ↔ B
  • バックエンド ↔ バックエンド(非同期処理、キュー、Webhook、内部APIなど)

この間を往来するデータは、しばしば次のように仮定されます。

"このURLは外部に公開されていないので、内部システムだけが呼び出す"。 "このトークン形式は私たちのサービスだけが知っているので安全だ"。

しかし攻撃者は常にこの"仮定"を破ろうとします。

ここでHMAC(HMAC-SHA256などのメッセージ認証コード)は、根本的な質問に答えるために存在します。

このリクエスト/メッセージは、実際に"私たちが持つ秘密鍵"を知っている誰かが作ったのか?

もう少し詳しく書くと:

  • HMACキーを知っている側だけが有効な署名(signature)を生成でき、
  • サーバーはpayload + signatureを受け取り、
  • "署名が合っているか"、
  • "途中で改ざんされていないか"を確認できます。

HMAC署名のない世界

HMAC署名のないREST/Webhook/内部APIはどうなるでしょうか?

  • 攻撃者がURL構造とパラメータをある程度把握したと仮定すると、
  • 任意のリクエストを作成して送信し、サーバーがそれを"自然な内部リクエスト"と勘違いさせることが可能です。

例えば、

POST /internal/run-action
Content-Type: application/json

{
  "action": "promoteUser",
  "userId": 123
}

元々は"バックオフィスからのみ呼び出す内部エンドポイント"だったとしても、

  • ネットワーク境界が一度突破される、
  • 内部プロキシの脆弱性がある、
  • CI/CD、ログ、サンプルコード、ドキュメントが外部に漏れる、

といった状況で、攻撃者は内部システムに装ってこのエンドポイントを叩くことができます。

このとき、HMAC署名があれば状況は完全に変わります。

  • リクエストボディ全体にHMAC署名を付与し、
  • サーバーは毎リクエストでsignatureを検証すれば、

"URLを知っても、パラメータを知っても、キーがなければ有効なリクエストを作れない"

という状態になります。


例:HMAC署名で内部アクションを保護する



簡単な例を見てみましょう。 (TypeScript/Node.js風の擬似コード)

1. サーバーが共有秘密キーを持っていると仮定

const HMAC_SECRET = process.env.HMAC_SECRET!; // .envやSecret Managerから注入

2. クライアント(または内部サービス)がリクエストを作るとき

import crypto from 'crypto';

function signPayload(payload: object): string {
  const json = JSON.stringify(payload);
  return crypto
    .createHmac('sha256', HMAC_SECRET)
    .update(json)
    .digest('hex');
}

const payload = {
  action: 'promoteUser',
  userId: 123,
};

const signature = signPayload(payload);

// 送信
fetch('/internal/run-action', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Signature': signature,
  },
  body: JSON.stringify(payload),
});

3. サーバーが検証するとき

function verifySignature(payload: any, signature: string): boolean {
  const json = JSON.stringify(payload);
  const expected = crypto
    .createHmac('sha256', HMAC_SECRET)
    .update(json)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expected, 'hex'),
  );
}

app.post('/internal/run-action', (req, res) => {
  const signature = req.headers['x-signature'];

  if (!signature || !verifySignature(req.body, String(signature))) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // ここまで来たということは:
  // 1) payloadが途中で改ざんされていない
  // 2) HMAC_SECRETを知っている主体が作ったリクエストである
  handleInternalAction(req.body);
});

この構造が、"このデータが本当に私たちの側から送られたものか"を証明する最も基本的なパターンです。


管理者(運営者/開発者)が複数いる場合:HMACキー回転は"選択ではなく必須"

HMACキーのもう一つの現実的な問題はです。

  • 運営者、SRE、バックエンド開発者、外注、インターン…
  • すべてが.envにアクセスできるわけではないが、
  • GitOps、CI/CD、ドキュメント、Slack、Notionなどどこかでキーが漏れる可能性は常にあります。

ここで重要なのは:

"キーが漏れる可能性がある"という仮定を前提に動く」

つまり、"いつか漏れる"と見なして設計すべきです。

そこでHMACキー回転(rotation)が必須になります。

なぜ回転が必要か

  1. 人が変わる * 退職、チーム変更、外注終了など * 過去にキーを見た人が今は見られないようにする必要がある。

  2. キー漏洩を完全に追跡できない * 過去のSlack DM、ローカルノート、キャプチャ、メモ… * "一度もキーを漏らしたことがない"という保証はほぼ不可能。

  3. 事故後も復旧可能な構造が必要 * もし特定時点以前のキーが漏れた場合、 * "その後は新キーでのみ署名されたリクエストを受け付ける"と宣言できる。

実務でのHMACキー回転パターン

管理者/運営者が複数いる組織では、通常次のパターンが推奨されます。

  1. キーのバージョン管理 * HMAC_SECRET_V1, HMAC_SECRET_V2 のようにバージョンを付け、 * リクエストヘッダーやペイロードにkid(key id)を一緒に送る。

  2. サーバーは複数キーを同時に保持し、検証時にバージョン別に処理 * 例:V1、V2をすべて検証可能にした後、 * 一定期間後にV1を廃止し、V2のみ許可。

  3. 回転プロセスを文書化 * "新キー生成 → 配布 → 両者(生成者/検証者)を新キーへ切り替え → 旧キー除去" の手順を * チェックリスト + ランブックとして作り、人が変わっても同じ手順で実行できるようにする。

  4. 権限分離 * キーを生成/管理する人と、アプリケーションコードを修正する人を分離することも可能なら検討。

HMACキーが"一度決めたら終わり"の値になる瞬間、 キー漏洩=システム全体の崩壊になります。

キー回転はこのリスクを時間に応じて限定する技術的手段です。


ゼロトラスト:"信頼は構造ではなく、各リクエストで作られる"

最後に、今回のReact RCE事件を通じて再確認すべきセキュリティの基本原則があります。

セキュリティの基本はゼロトラスト(Zero Trust)です。

ゼロトラストの核心は非常にシンプルです。

  • "内部だから大丈夫だろう"。
  • "フロントでのみ使う値だから安全だろう"。
  • "私たちが作ったフレームワークだから自動で防げる"。

このような文言を基本的に禁止する思考です。

代わりに次のように質問すべきです。

  • "この入力は、悪意があると仮定して設計されたか"?
  • "このリクエストが本当に私たちが意図した主体から来たという証拠はあるか"?
  • "このデータが途中で改ざんされていないことをどう確認するか"?
  • "このキーが漏れたら、どの範囲に影響が及び、どう回収・復旧できるか"?

React RCE事件も同じフレームで見ることができます。

  • RSC Flightデータは"Reactが提供するものだから"という信頼の下で処理されました。
  • その結果、クライアントが操作できる値がサーバーのモジュールロードパスに直接触れていた

ゼロトラストの観点から見ると、この構造は最初からこう見えるべきでした。

"クライアントが操作可能な直列化データがサーバーコード実行経路に影響を与える = 常に危険シグナル"

そしてこの場所で最初に思い浮かべるべき防御策の一つが、

  • HMAC署名(整合性・出所確認)
  • キー回転(キー漏洩を想定した被害範囲制限)

です。


まとめ:"信頼しないで、証明させる"

今回の記事で伝えたい核心メッセージは、まさに三つです。

  1. データを基本的に信頼しない * 特に、"フレームワークが内部で使うデータ"であっても、 * ネットワークを横断したならいつでも攻撃表面になる

  2. HMAC署名は"このデータが本当に私たちの側から来たか"を確認する基本ツール * REST API、Webhook、内部RPC、非同期メッセージなど、 * "サーバー間通信"があるすべてのポイントにHMACを検討。 * 管理者が複数いる環境では、キー回転をプロセスレベルで必須に。

  3. セキュリティの基本はゼロトラスト * "内部だから大丈夫だろう"という言葉が出る瞬間、すでに危険サイン。 * 信頼は構造設計で自動付与されるものではなく、 * 各リクエスト、各データごとに検証を通じて新たに作られるものです。

React RCE事件は、フロントエンドとサーバーの境界が曖昧になる時代に、 私たちがどのような姿勢を取るべきかを改めて思い起こさせてくれます。

"信頼しないで、検証しよう。検証をコード、プロセス、キー管理ポリシーとして残そう。"

その第一歩がHMAC署名とキー回転であり、その上に立つ哲学がゼロトラストです。

image