postMessage 有哪些安全风险?前端如何正确校验?
简化版
postMessage 用于跨窗口、iframe、Worker 等场景通信,安全关键是发送时指定精确 targetOrigin,接收时校验 event.origin、event.source 和消息结构。不要使用 * 发送敏感数据,也不要把收到的数据直接写入 DOM 或当作命令执行。
详细版
安全接收示例:
const allowedOrigins = new Set(['https://pay.example.com'])
window.addEventListener('message', event => {
if (!allowedOrigins.has(event.origin)) return
if (event.source !== document.querySelector('#pay-frame').contentWindow) return
if (!event.data || event.data.type !== 'PAY_RESULT') return
renderPayResult(event.data.payload)
})
常见漏洞是接收端不校验来源、发送端使用 targetOrigin='*'、消息类型没有白名单、把消息内容拼进 innerHTML。面试要把它和同源策略联系起来:postMessage 是浏览器提供的受控跨源通道,安全性取决于你是否把通道边界验证好。
完整版教学
一、postMessage 为什么需要安全校验
同源策略限制一个页面直接读取另一个源的 DOM,但业务又经常需要跨源 iframe 通信,例如支付 iframe、单点登录弹窗、嵌入式客服和第三方报表。postMessage 就是浏览器给出的显式通信通道。
main.example.com
└─ iframe: pay.example.com
-> window.parent.postMessage({ type: 'PAY_RESULT' }, 'https://main.example.com')
记忆钩子:
postMessage打开了一扇门,targetOrigin和event.origin就是门牌号和验票口。
二、发送端要写精确 targetOrigin
iframe.contentWindow.postMessage(
{ type: 'INIT', orderId: 'A001' },
'https://pay.example.com',
)
第二个参数不是装饰,它决定消息允许发给哪个 origin。如果写成 *,当 iframe 被导航到恶意站点时,敏感消息仍可能发过去。假设支付 iframe 原本是 pay.example.com,后来被重定向到 evil.com,targetOrigin='*' 会让订单信息落到攻击者窗口。
只有在消息完全不敏感、且接收方确实不固定时,才考虑 *。即使如此,接收方也必须严格校验。
三、接收端校验三件事
| 校验项 | 作用 | 常见错误 |
|---|---|---|
event.origin | 确认消息来自哪个源 | 用 includes('example.com') 模糊匹配 |
event.source | 确认来自哪个窗口对象 | 只校验 origin,不管窗口来源 |
event.data | 确认消息结构和类型 | 任意字段都信任 |
function isPayMessage(data) {
return data &&
data.type === 'PAY_RESULT' &&
typeof data.payload?.orderId === 'string' &&
['success', 'failed'].includes(data.payload.status)
}
origin 应使用精确白名单,不要用字符串包含判断。https://example.com.evil.com 也包含 example.com,但它不是你的可信源。
四、消息数据不能直接进入危险 API
window.addEventListener('message', event => {
if (event.origin !== 'https://trusted.example') return
box.innerHTML = event.data.html // 危险:仍可能变成 DOM XSS
})
来源可信不等于内容永远安全。可信系统也可能被配置错误、被 XSS 污染或传回用户可控内容。收到消息后仍要按数据类型处理:文本用 textContent,HTML 需要可靠净化,命令类消息必须走白名单。
五、设计消息协议
一个稳妥的消息协议至少包括类型、版本、请求 id 和载荷:
{
type: 'PAY_RESULT',
version: 1,
requestId: 'req-001',
payload: { orderId: 'A001', status: 'success' }
}
这样做的好处是可以防止不同业务消息混用,也方便超时、重试和回包匹配。若一次页面里有 3 个 iframe,同一个 PAY_RESULT 必须能对应到正确订单,否则就可能把 A 用户的状态渲染到 B 订单上。
六、与 iframe 沙箱配合
iframe sandbox 可以限制嵌入页面的能力,例如脚本、表单、弹窗、同源权限等。postMessage 负责通信,sandbox 负责降低被嵌入内容失控后的影响面。
| 配置 | 含义 |
|---|---|
sandbox | 默认开启多种限制 |
allow-scripts | 允许脚本执行 |
allow-forms | 允许表单提交 |
allow-same-origin | 保留原 origin,需谨慎 |
如果给第三方 iframe 同时加 allow-scripts 和 allow-same-origin,要特别小心,因为它可能重新获得较强能力。前端安全不是只靠一个 API,而是组合边界。
七、常见误区与追问
- 误区:
postMessage天然安全。 它只是提供跨源通信能力,安全取决于发送和接收校验。 - 误区:
targetOrigin='*'没问题。 发送敏感数据时应指定精确 origin,避免窗口被导航后泄露。 - 误区:校验
origin.includes()就够了。 字符串包含会被伪造域名绕过,应使用精确白名单。 - 追问:为什么还要校验
event.source? 同一个可信 origin 可能有多个窗口,source 能进一步绑定预期窗口。 - 追问:收到可信源的 HTML 能直接渲染吗? 不能,仍要防 DOM XSS 和上游污染。
- 追问:如何处理响应对应关系? 使用
requestId、超时和类型白名单设计消息协议。
八、加强记忆
postMessage 安全答题可以按“发准、收严、用稳”记:发送指定精确 targetOrigin,接收校验 origin/source/data,使用时按白名单和安全 DOM API 处理。它不是同源策略的漏洞,而是浏览器提供的受控跨源通道;控制不严,通道就会变成攻击入口。