postMessageの受信時にオリジンを検証しない問題

postMessageの受信時にオリジンを検証しない問題

説明

postMessage の受信側が、送信元ウィンドウのスキーム・ホスト・ポートを表す event.origin を厳密に検証しないと、悪意のあるサイトからのメッセージを信頼するおそれがあります。攻撃者は対象ウィンドウへの参照を取得し、メッセージを届けられる必要があります。ハンドラーが eval で内容を実行したり、権限のある操作を行ったりすると、機能の悪用、情報漏えい、コード実行につながる可能性があります。

想定される影響

  • 信頼できないメッセージによって、機密情報が外部へ送信されたり、コンソールやDOMに露出したりするおそれがあります。
  • eval、Function や重要な関数を呼ぶハンドラーで、攻撃者が選んだ処理を実行される可能性があります。
  • 認証済みのコンテキストで命令が実行され、ユーザーの権限を悪用した操作や状態変更が起こるおそれがあります。
  • 大量のメッセージでイベントキューやブラウザーのリソースを圧迫される可能性があります。

対処方法

  • event.origin を許可したスキーム・ホスト・ポートと完全一致で比較してください。startsWith や includes の部分一致だけで許可しないでください。
  • 想定する送信元が分かる場合は、event.source を特定の iframe.contentWindow などとも比較してください。
  • event.data を解析し、想定するスキーマと許可したフィールドを検証してください。eval や new Function で実行しないでください。
  • 検証に失敗したら、何も処理せずreturnしてください。ログを残すだけでは拒否になりません。
  • 機密のメッセージを送る場合は、* ではなく正確な targetOrigin を指定してください。

例

変更前

javascript
// オリジンを検証せず、危険なAPIや操作を実行
window.addEventListener("message", (e) => {
  // 送信元ウィンドウを確認せず、届いたメッセージを信頼
  if (e.data && e.data.type === "setTheme") {
    document.documentElement.setAttribute("data-theme", e.data.value);
  }

  // 危険: 受信データを実行
  if (e.data && e.data.exec) {
    eval(e.data.exec); // 任意のコードを実行される可能性
  }

  // トークンなどの機密情報を設定・送信
  if (e.data && e.data.accessToken) {
    localStorage.setItem("access_token", e.data.accessToken);
  }
});

変更後

javascript
// オリジン・想定する送信元・データ形式を検証
(() => {
  const ALLOWED_ORIGINS = new Set([
    "https://app.example.com",
    "https://admin.example.com",
  ]);

  // 想定する送信元ウィンドウが分かる場合は参照を保持
  const settingsFrame = document.getElementById("settings-frame");
  const expectedSource = settingsFrame ? settingsFrame.contentWindow : null;

  function isValidPayload(payload) {
    // 最小限の形式検証: オブジェクトと許可したメッセージ型を要求
    if (!payload || typeof payload !== "object") return false;
    const allowedTypes = new Set(["setTheme", "ping"]);
    return typeof payload.type === "string" && allowedTypes.has(payload.type);
  }

  window.addEventListener("message", (event) => {
    // 1) オリジンの完全一致を確認
    if (!ALLOWED_ORIGINS.has(event.origin)) return;

    // 2) 参照がある場合は送信元ウィンドウを確認
    if (expectedSource && event.source !== expectedSource) return;

    // 3) データを検証して処理
    const data =
      typeof event.data === "string"
        ? (() => {
            try {
              return JSON.parse(event.data);
            } catch {
              return null;
            }
          })()
        : event.data;

    if (!isValidPayload(data)) return;

    switch (data.type) {
      case "setTheme":
        if (
          typeof data.value === "string" &&
          /^(light|dark)$/.test(data.value)
        ) {
          document.documentElement.setAttribute("data-theme", data.value);
        }
        break;
      case "ping":
        // 検証済みの正確なtargetOriginを指定して応答
        event.source &&
          event.source.postMessage(
            JSON.stringify({ type: "pong" }),
            event.origin
          );
        break;
      default:
        // no-op
        break;
    }
  });
})();

解説:

  • 変更前: オリジンを確認せずメッセージを信頼し、受け取ったコードの実行や設定・トークンの変更を行います。メッセージを届けられる攻撃者に、その処理を悪用されるおそれがあります。
  • 変更後: オリジンを許可リストと完全一致で比較し、想定するiframeの参照がある場合は送信元も確認します。特定のウィンドウだけを許可する必要があれば、その参照がない状態で処理しないでください。解析したデータの形式と値を検証し、不正なメッセージは危険な実行APIを使わずに拒否します。

参考資料