説明
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を使わずに拒否します。