← 中文资源

简体中文版本

postMessage 静默失效 — iframe 的 origin 不匹配

调试永远到不了的对端 postMessage 消息,排查 targetOrigin 与 event.origin 不一致,验证握手两侧并严格校验。

阅读英文原文

症状

  沉默本身就是 bug:发送方打了日志、接收方毫无动静、两侧控制台都没有异常。机制是——带具体 targetOrigin 时,浏览器在投递时刻将其与收方文档 origin 比对,不一致就丢弃消息,按规范静默执行。接收侧自有陷阱:handler 校验 event.origin 对照白名单,而过期的名单(旧域名、http、www 与裸域之差)会把合法发送者也同样悄悄忽略。主域正常、预览域或子域静默失败,是域名变体的标准表现。

诊断思路

  给每次交换两侧都打日志:发送方记 targetOrigin 与数据,接收方记 event.origin 与数据——有发送日志而无消息事件即浏览器丢弃,日志里的 origin 对直接点名失配;再在父页与 iframe 两个上下文里各打 location.origin,scheme、host、port 必须逐字符一致,一个尾斜杠或 www 之差足以让消息无声蒸发;最后确认监听器注册时机——iframe 内部导航后或监听器注册前发出的消息即使 origin 正确也会消失,竞态是孪生变体。

根因

  带具体 targetOrigin 的 postMessage 只在收方窗口文档 origin 与之精确匹配时投递,否则浏览器无任何错误地丢弃。接收侧 event.origin 反映发送方真实 origin,严格校验对白名单外的来源静默忽略。两侧必须对确切的 origin 字符串达成一致。

修复与验证

  出口用精确 origin:真实载荷绝不传 ‘*’(通配会把内容泄漏给投递时刻占据框架的任何文档);入口用显式白名单并经配置同步(apex、www、preview 都在名单里,域名变更改配置而不是两处硬编码);握手排序:父页等 iframe load 后再发 init,iframe 注册监听器后立即发 ready,闭合“早到消息找不到监听器”的竞态。验证时在每个域名变体上跑双向握手测试,消息交换且校验正确;再喂一个伪造 origin 确认被拒绝。

代码示例

iframe.contentWindow.postMessage({ type: 'init' }, '*');
// -> no message event in the iframe, no error in either console
iframe.contentWindow.postMessage(
  { type: 'init', token },
  'https://widget.example.com'    // never '*' for real payloads
);
const ALLOWED = new Set([
  'https://app.example.com',
  'https://preview.example.com',  // kept in sync via config, not hardcoded twice
]);

window.addEventListener('message', (event) => {
  if (!ALLOWED.has(event.origin)) {
    console.warn('rejected message from', event.origin);
    return;
  }
  handle(event.data);
});