1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

postMessage の targetOrigin が相手と違うと、例外も出ずにメッセージが捨てられます。iframe 自身のオリジンを入れていました【Chromium 141 で実測】

1
Posted at

iframe の中から parent.postMessage(msg, targetOrigin) を呼ぶとき、targetOrigin が親ページのオリジンと違うと、メッセージは捨てられます。例外は投げられず、postMessage() は普通に戻ってきます。残るのは、開発者ツールのコンソールの警告が1行だけです。

別のポートで親ページ(http://127.0.0.1:8801)と iframe(http://127.0.0.1:8802)を用意し、iframe から親へ、targetOrigin を変えて送りました。Playwright 経由の Chromium 141.0.7390.37 です。

targetOrigin 例外 親に届いたか コンソール
http://127.0.0.1:8802(iframe 自身のオリジン) なし 届かない 警告1行
http://127.0.0.1:8801(親のオリジン) なし 届いた なし
* なし 届いた なし
http://127.0.0.1:8801/some/path?x=1 なし 届いた なし
http://127.0.0.1:8801/ なし 届いた なし
http://localhost:8801(ホスト名だけ違う) なし 届かない 警告1行
https://127.0.0.1:8801(スキームだけ違う) なし 届かない 警告1行
/ なし 届かない 警告1行
not-a-url SyntaxError 届かない なし

警告の文はこうです。

Failed to execute 'postMessage' on 'DOMWindow': The target origin provided ('http://127.0.0.1:8802') does not match the recipient window's origin ('http://127.0.0.1:8801').

/ は「送る側と同じオリジン」という意味なので、iframe 自身のオリジンを書いたのと同じ結果でした。パスやクエリは無視され、比べられるのはスキーム・ホスト・ポートだけです。オリジンとして読めない文字列だけは、例外になりました。

先に断っておくと、targetOrigin が一致しないメッセージを送らないのは、HTML の仕様どおりの動きです。MDN の postMessage() のページにも、一致しなければイベントは送られないと書かれています。ここで書くのは、自作の WordPress プラグインでこれを踏んで、閉じるボタンが別のサイトでだけ効かなかった話と、targetOrigin の書き方ごとに測った結果です。

何が起きていたか

自作の Rapls AI Chatbot には、チャットを別のサイトに埋め込むためのスクリプトがあります。埋め込むと、チャットは iframe の中に入ります。チャットの × ボタンを押すと、iframe から親ページに「閉じて」とメッセージを送り、親ページが iframe を隠します。

1.19.1 まで、その送り方はこうでした。

var targetOrigin = this.config.embed_origin || window.location.origin;
window.parent.postMessage({type: 'raplsaich:close'}, targetOrigin);

embed_origin が無いときは、window.location.origin を使います。でも、iframe の中の window.location.origin は、iframe 自身のオリジン(チャットを置いている WordPress のサイト)です。

同じサイトの中に埋め込んだときは、親と iframe のオリジンが同じなので、届きます。別のサイトに埋め込むと、表の1行目と同じことになり、メッセージは捨てられます。例外が出ないので、× ボタンは「押しても何も起きない」だけでした。

1.19.2 の直し方

親ページのオリジンを、iframe の URL に付けて渡すようにしました。

// 埋め込み用スクリプト(親ページ側)
iframe.src = siteUrl + '/?raplsaich_embed=1&raplsaich_parent=' +
    encodeURIComponent(window.location.origin);

iframe を返す PHP 側で、それがオリジンの形をしているかを確かめてから、targetOrigin に使います。形が違えば * にします。

$embed_parent = isset($_GET['raplsaich_parent']) ? sanitize_text_field(wp_unslash($_GET['raplsaich_parent'])) : '';
$embed_origin = preg_match('#^https?://[A-Za-z0-9.\-]+(:\d{1,5})?$#', $embed_parent) ? $embed_parent : '*';
var targetOrigin = this.config.embed_origin || '*';

* に逃がしてよいのは、送る中身が「閉じて」「準備できた」という合図だけで、知られて困るものを含まないからです。受け取る親ページの側でも、event.origin がチャットのサイトであることを確かめてから動きます。秘密を含むメッセージなら、* にしてはいけません。

親のオリジンを知る方法

URL で渡す以外にも、iframe の中から親のオリジンを知る方法はあります。同じ構成で読んでみました。

document.referrer             // "http://127.0.0.1:8801/"
location.ancestorOrigins[0]   // "http://127.0.0.1:8801"
location.origin               // "http://127.0.0.1:8802"(iframe 自身)

親ページの URL には ?secret=1 を付けていましたが、document.referrer にはオリジンと / しか入っていませんでした。Chromium の既定の Referrer-Policy(strict-origin-when-cross-origin)で、別オリジンにはオリジンだけが渡るためです。親ページが Referrer-Policy を no-referrer にしていれば、空になります。location.ancestorOrigins は、ブラウザによって使えないものがあります(今回測ったのは Chromium だけです)。どちらも、いつでも頼れるわけではないので、埋め込むスクリプトの側から明示的に渡すことにしました。

postMessage を使うとき

  • targetOrigin が一致しないメッセージは、例外なく捨てられる。コンソールに警告が1行出るだけ
  • iframe の中の window.location.origin は、iframe 自身のオリジン。親のオリジンではない
  • 比べられるのはスキーム・ホスト・ポート。パスは無視される。localhost と 127.0.0.1 は別物
  • / は「自分と同じオリジン」。iframe から親に送ると、別オリジンなら届かない
  • 親のオリジンは、埋め込む側から渡すのがいちばん確実。受け取る側は event.origin を必ず確かめる
  • * は、知られて困らない合図にだけ使う

あなたの iframe は、親ページに「どこへ」送っていますか。

手元で試すコマンド

PHP のビルトインサーバーを2つ立てて、Playwright で送ります。

<!-- a/index.html(親ページ。8801 で配る) -->
<!doctype html><meta charset="utf-8">
<script>
window.got = [];
addEventListener('message', e => window.got.push({ data: e.data, origin: e.origin }));
</script>
<iframe id="f" src="http://127.0.0.1:8802/frame.html"></iframe>
<!-- b/frame.html(iframe。8802 で配る) -->
<!doctype html><meta charset="utf-8">
<script>
window.trySend = (label, target) => {
  try { parent.postMessage(label, target); return 'sent'; }
  catch (e) { return e.name + ': ' + e.message; }
};
</script>
// run.js
const { chromium } = require('playwright');
(async () => {
  const b = await chromium.launch();
  const p = await b.newPage();
  const logs = [];
  p.on('console', m => logs.push(m.type() + ': ' + m.text()));
  await p.goto('http://127.0.0.1:8801/index.html');
  const fr = await (await p.$('#f')).contentFrame();
  await fr.waitForFunction(() => typeof window.trySend === 'function');
  for (const t of ['http://127.0.0.1:8802', 'http://127.0.0.1:8801', '*', 'http://localhost:8801', '/', 'not-a-url']) {
    const before = logs.length;
    const r = await fr.evaluate(([l, t]) => window.trySend(l, t), [t, t]);
    await p.waitForTimeout(300);
    const got = await p.evaluate(l => window.got.some(g => g.data === l), t);
    console.log(t, r, got ? '届いた' : '届かない', logs.slice(before).join(' | '));
  }
  await b.close();
})();
(cd a && php -S 127.0.0.1:8801 &) ; (cd b && php -S 127.0.0.1:8802 &)
node run.js

ふだんはraplsworks.comで、WordPressプラグイン開発やClaude Codeまわりのことを書いています。
https://raplsworks.com/

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?