ARTICLE · 995213
登录弹窗、支付 iframe、客服插件:一条被忽略的前端攻击面
你的网站每天都在和陌生人「写信」,门却经常没锁
登录弹窗、支付 iframe(小窗口)、客服插件背后,有一条很多人没认真看过的攻击面:
1. 打开一个网站,点「用微信登录」,会弹出一个新窗口。
2. 再点支付,页面里可能嵌着一个小小的收银台。
3. 右下角那颗客服气泡,多半也是别人家的脚本,挂在你自己的域名上。
这三件事看起来毫无关系。 对浏览器来说,它们做的是同一件事:让两个不同源的页面说话。
负责这件事的 API,叫 postMessage。
它不是新东西,HTML5 时代就有了。 开发用了十年,产品早已离不了它,安全同学却经常只在扫描器结果里见过这个词,然后标个「低危配置」就划走。
划走有点可惜。
近两年公开的案例里,这条信道里流出过登录授权码、云平台通行证、观看记录等;拼起来,还能打到用户无感知的账户接管。
不是因为 postMessage 本身是漏洞,而是因为:浏览器把「谁可以说话」这件事,完全交给了业务自己判断。
判断写错,门就是开的。
01 先把这件事说成人话
浏览器有一条老规矩,叫同源策略。
简单说:A 网站的脚本,不能去翻 B 网站的抽屉。
Cookie、DOM、本地存储,默认都隔开。
可产品偏偏需要跨过去:
登录窗口要把「用户同意了」告诉原来的页面。
支付 iframe 要把「扣款成功」告诉商户站。
客服插件要把「打开会话」告诉主站。
于是浏览器开了一扇官方许可的窗:你可以给另一个窗口寄一封信。
这封信有四件东西:
信的内容。想写什么写什么,字符串、对象都行。
收件窗口。弹窗、iframe、父页面、opener,都算。
收件地址,也就是 targetOrigin。写具体域名,信只送到这个源;写成一个星号 *,等于「谁在这个窗口里都送到」。
寄件人印章。浏览器会盖上,伪造不了,接收方能看见这封信到底从哪个源来。
注意最后一句: 浏览器只保证印章是真的。 它不保证你验不验这枚印章,也不保证你验完之后,会不会把信里的字直接当代码执行。
很多人觉得「跨源通信是浏览器的安全能力」。
其实恰好相反。postMessage 是同源策略的豁免口。
豁免口上的保安,是开发自己。
02 它在你的产品里,可能比你以为的更常见
你不一定在业务代码里亲手写过这行 API。
可只要产品里有下面这些东西,信道多半已经在了:
第三方登录、扫码登录、App 拉起 H5 登录
支付、分账、发票弹层
右下角客服、工单、IM 分享组件、嵌入播放器、文档预览
微前端、门户里套的子系统
统计 SDK、同意管理、广告回传
会议、直播、实名里申请过摄像头或麦克风的页面
它们有一个共同点:两个窗口必须协作,还不能直接碰对方的 DOM。 于是都走到同一条路上。
赏金猎人喜欢这条路,不是因为炫技。 是因为它同时满足三个条件:出现频率高、校验经常写错、一旦接到登录或支付,影响很好讲。
03 真正的攻击面,不是一个函数,是四层门
把 postMessage 想成公司前台的对讲机,比较好懂。
第一层,接收端:谁可以对着你喊指令。
页面挂了消息监听,却不看寄件人印章,或者看了但用「地址里包含某某公司」这种模糊办法比。 结果就是:不该进门的人,对着你的页面喊一句,页面真的照做。
照做的内容如果被写进页面、拿去跳转、拿去加载脚本,就是前端 XSS;
如果被拿去改邮箱、确认支付、拉起摄像头,危害直接落到业务上。
第二层,发送端:你是不是把秘密广播出去了。
登录回跳页最常见的一种写法,是把当前地址或者 token 往父窗口一扔,收件地址还写成星号。
星号的意思是:这个窗口现在是谁,信就给谁。
用户如果是从别人准备好的页面点进来的,秘密就会送到别人的信箱里。 这里甚至不需要接收端有漏洞。发送端自己就把东西递了出去。
第三层,管道:以为接通了专线,其实专线可以被人插走。
有一种更「现代」的用法,叫 MessageChannel。
两个窗口先握一次手,之后走专用端口,后续消息甚至不再带寄件人印章。 信任必须在握手那一瞬间建完。 如果握手时只核对一句写在公开 JS 里的口令,不核对对方是谁,专线就会被插到别人家里。 公开研究里,已经出现过把 SSO 放进隐藏 iframe、把授权码从这条专线上接走的案例。影响可以到账户接管。
第四层,门禁:别人能不能把你的页面,放到他们的窗口树里。
不能被嵌、不能被弹窗,很多接收端问题走不通。
所以安全头不是装饰。X-Frame-Options、CSP 的 frame-ancestors、跨窗口的 COOP,决定的是「这台对讲机有没有机会被拿到别人办公室里用」。 门禁严了,攻击者可能改走弹窗而不是 iframe;门禁松了,隐藏 iframe 就能把整条链转起来。
四层里任何一层松了,都值得看。 四层叠在一起,才是赏金报告里那些「看起来不像一个洞、金额却不低」的组合。
04 开发最常写错的,其实就四类
第一类,监听器存在,但根本不看寄件人。
代码里搜不到 origin。 这不等于已经是漏洞,它只说明:谁都可以把信塞进来。下一步要看这封信会不会被当成指令。
第二类,看了,但比较方式是「差不多就行」。
用包含、前缀、后缀、没写锚点的正则去比域名,是最典型的纸门。 你以为自己只收总部来信,实际上「名字里带总部」的地址都能进。 安全圈里反复出现过一种更细的分叉:业务用字符串规则检查用户传入的地址,浏览器真正投递时却会先解析 URL。两边看到的不是同一个东西。地址看起来还像自家域名,信已经送到别人那里。
第三类,收件地址写成星号。
开发图省事,不知道对端到底是哪个源,就对所有人广播。 消息内容如果只是「我准备好了」「窗口改大小了」,通常值不了几个钱。
消息内容如果是 token、授权码、当前带 code 的 URL、用户资料,这就是泄密,不需要再配一个 XSS。
第四类,只信「这是父窗口发的」,或者只信信里的一句口令。
父窗口是谁,取决于谁把你嵌进去。 口令如果写在前端公开脚本里,谁都能念。 身份只能来自浏览器盖上的那枚印章,不能来自信件正文,也不能来自「我是你爸」。
这四类错,单独拿出来都像配置问题。 接上登录、支付、已授权的摄像头,叙事就变了。
05 几个公开故事,用来建立直觉
不必记厂商名,记「它错在哪一层」就够。
有大厂的云平台相关页面,把带权限的访问令牌,用星号发给 iframe。 错在发送端。信是真的,收件人却是「谁都行」。
有大型内容产品的网页版,消息处理没正确核对来源,被证明可以取回观看记录、账号信息和搜索词。 错在接收端。不需要执行脚本,泄密本身已经成立。
有 OAuth 回跳把目标地址放在 state 参数里,用「以某某域名结尾」来判断是不是自己人,再把当前页面地址寄回去。 错在比较方式和发送端叠在一起。后缀对了,人不对,授权码就出门了。 这类问题进过厂商的漏洞奖励,金额可以到万美元级别。
还有研究披露过更完整的组合:
登录框能被任意网站嵌进去,就绪消息对外广播,接收专用端口时只对口令不对人,PKCE 的挑战值还由嵌入方说了算。 单看每一环,都像「实现细节」。串起来,就是零点击接管:任意站点把 SSO 藏进隐藏 iframe → 听到对外广播的就绪 → 用公开口令把专线插到自己这边 → 用户如果在这个 SSO 上已经登录,iframe 里会自动放码 → 码走进攻击者的 Port → 挑战值也是攻击者生成的,兑换能过。
用户侧常常是:某天打开过一个无关网页,自己没再点「同意」,SSO 会话还在,号在另一边已经登录完成。所以叫零点击接管
这些故事有同一个教训:postMessage 很少因为「少写了一行 if」本身值很多钱。 它值钱,是因为现代登录和支付,本来就把最敏感的东西放在这条信道上。
06 如果你是开发,先做这几件最小的事
接收的时候,用完整源做精确匹配,不要用包含、前缀、后缀去「感觉对」。 明确拒绝来源是 null 的信。sandbox 和 data 页面会把印章盖成这个字。
先看章,再拆信。 信拆开之后当数据用,不当代码用。不要把消息内容写进页面 HTML,不要拿去 eval,不要丢给 javascript: 地址。
发送敏感数据时,收件地址写成常量。 不要写星号,更不要把用户 URL 里的 origin 原样丢给发送函数。 如果业务必须从参数里读一个地址,先按 URL 解析,再拿解析后的源去对白名单,最后把解析结果交给浏览器。不要让两套解析器各看各的。
登录、支付、SSO 这类页,加上谁能嵌你的限制。 不需要跨源说话的页面,把监听器拿掉。这是最省事、也最不容易写错的修复。
07 如果你是测试,判断标准可以很简单
先找有没有人在听,有没有人在说。 浏览器开发者工具里,Sources 下面的 Global Listeners,能看到当前页挂了哪些消息监听。每个 iframe 都要看一眼,洞经常不在最外层。
找到之后,只问四个问题:
听的人核不核对章? 核对方式是精确匹配,还是模糊串? 说出去的信,收件地址是写死的,还是星号,还是来自用户参数? 信里的内容最后干什么去了?写进页面、拿去跳转、发出去当 token,还是只是一句无意义的 ready?
前三个问题帮你判断门是不是纸做的。 第四个问题决定它是不是洞。
这里有一条很重要的赏金常识: 少写一次来源校验,通常只是 gadget,不是报告。 审核员要看的是受害者损失,不是 API 用得不够优雅。 跨站 iframe 里还经常读不到 SameSite=Lax 的登录 Cookie,所以「能弹个框」不等于「能偷登录态」。 讲得清楚的报告,会写用户怎么碰到这个页面、信道在哪、数据最后变成了什么、在自己的测试号上验证了哪一步。
国内项目还多问一句:这段脚本是不是第三方,范围包不包括。 很多 SDK 把监听器种在你的域名下,看起来像别人的问题,Cookie 却是你的。 但项目如果写明第三方不收,争也争不回来。先看规则,再写报告。
08 为什么值得单独拿出来说
前端安全的公共知识,过去十年几乎被 XSS、CSRF、点击劫持三分天下。postMessage 不像它们那么有名字,却正好长在登录和支付最需要跨窗协作的位置上。
它也不只是前端的事。 Electron、小程序 web-view、App 里的 JSBridge,很多都是同一套信任模型的孪生兄弟:一端以为对面是自己人,一端把指令和秘密放上了桥。
所以与其把它记成一个 API 名称,不如记成一句话:
凡是两个窗口必须隔着同源策略协作的地方,都有一条指令总线,和一条泄密总线。 总线在不在,看产品形态。 总线危险不危险,看四层门。 值不值得写进报告,看数据最后去了哪。
你不一定要成为挖这类洞的人。 但如果你负责登录、支付、嵌入和第三方插件,至少该知道:网站每天都在写信,章是真的,门却可能是开的。
打开自家后台,搜一搜 addEventListener('message' 和 postMessage(。 如果搜到了,再问一句:这封信,真的只送给该收的人了吗?