Skip to content

postMessage 通信安全、前端供应链安全与敏感信息泄露

难度等级:🔴 高级(3-5年+)
分类:浏览器安全
归档日期:2026-09-03


📚 理论知识

一、postMessage 跨窗口通信的安全隐患

背景postMessage 是浏览器提供的、专门用于跨窗口/跨 iframe/跨域安全通信的 API,天生就是设计来"绕过"同源策略限制的能力——两个不同源的窗口本来互相访问不了对方的 DOM,但可以通过 postMessage 互相发消息。正因为它本身就是同源策略的一个"官方缺口",用得不对就会变成实实在在的安全漏洞。

隐患一:发送方不指定精确的 targetOrigin

javascript
// 危险写法:目标源写成通配符
otherWindow.postMessage(sensitiveData, '*');

postMessage 的第二个参数是目标窗口的源,如果写成 *,意味着"不管这个 otherWindow 引用现在实际指向哪个页面,都把消息发过去"。如果这个窗口引用后来被跳转到了恶意页面(比如页面里有一个可以被诱导跳转的 iframe/新窗口),敏感数据就会被发送给了攻击者控制的页面。正确做法是始终指定明确、具体的目标源。

隐患二:接收方不校验 event.origin

javascript
// 危险写法:没有校验消息来源
window.addEventListener('message', (event) => {
  document.body.innerHTML = event.data.html; // 直接信任并使用
});

任何页面只要拿到了目标窗口的引用(比如通过 window.open() 返回值,或者自己就是被嵌入的 iframe 的父窗口),都能向目标页面发送 message 事件。如果接收方的回调不做来源校验就直接信任消息内容——比如直接拿去更新登录状态、拼进 DOM、甚至 eval——攻击者构造的恶意页面就能伪造消息,达成 XSS 注入或者业务逻辑绕过(比如伪造"支付成功"的消息)。

正确姿势

  1. 发送时始终指定精确的 targetOrigin,不用 *(除非消息本身完全不敏感、且明确知道后果)。
  2. 接收时,message 事件回调的第一行代码永远是校验 event.origin 是否在白名单内,不符合直接 return
  3. 消息内容本身也要当作不可信输入处理,不能因为"来自 postMessage"就默认它是安全的结构化数据,仍然要做校验和转义,不要直接拼进 innerHTML 或传给 eval

二、前端供应链安全

定义:项目依赖的第三方 npm 包、CDN 资源在没有被你本地代码修改的情况下,其源头本身被投毒或篡改,恶意代码通过"正常的依赖安装/资源加载"这个环节混入你的项目。因为是"信任链"层面的攻击,往往很隐蔽,一旦得手影响面是全量用户,是近几年增长非常快的攻击面(业界比较知名的案例如 event-stream 包被植入恶意代码、大规模 CDN 劫持事件等)。

具体风险点

  • npm 包被官方账号劫持后发布恶意版本;
  • 依赖的依赖(间接依赖)里被投毒,项目本身根本没有直接引用这个包,风险不易被察觉;
  • 公共 CDN 上托管的第三方脚本文件被篡改(CDN 服务商被攻破,或者中间人劫持了 CDN 请求)。

防御手段

  1. lockfile 锁定精确版本package-lock.json / yarn.lock / pnpm-lock.yaml):避免 ^/~ 这类语义化版本范围在 npm install 时自动升级到一个被投毒的新版本,锁文件保证每次安装的都是同一个哈希校验过的确定版本。

  2. 定期扫描已知漏洞依赖npm audit / pnpm audit,或者接入 Snyk、Dependabot 这类自动化工具,在 CI 流程里加一道依赖漏洞扫描的关卡。

  3. CDN 引入的第三方脚本用 SRI(Subresource Integrity)

    html
    <script
      src="https://cdn.example.com/lib.js"
      integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
      crossorigin="anonymous">
    </script>

    浏览器加载脚本后会计算它的哈希值和 integrity 属性里声明的哈希做比对,一旦 CDN 上的文件内容被篡改(哪怕改一个字节),哈希对不上,浏览器会拒绝执行这个脚本。

  4. 审查新增依赖:关注依赖包的下载量、维护活跃度、最近是否有可疑的版本发布记录,减少不必要的依赖引入,间接依赖也在攻击面之内,不能只审查直接依赖。

三、前端敏感信息泄露

常见泄露点

  • 打包产物里硬编码了密钥:把数据库密码、第三方服务的 Secret Key、内部接口的鉴权密钥直接写进前端代码。前端代码在浏览器里对所有人可见(哪怕经过压缩混淆,也只是增加阅读难度,不是加密,任何人都能在浏览器 DevTools 或直接下载 bundle 文件里找到明文)。
  • 生产环境误开 sourcemap:sourcemap 会把压缩混淆后的代码映射回未压缩的原始源码,如果生产环境的 sourcemap 文件可以被任意访问,等于把完整的、带注释的源码和内部实现逻辑直接暴露给了所有人。
  • 接口返回了超出前端展示需要的字段:比如登录接口把用户完整的手机号、身份证号、内部权限配置全部返回,即使前端页面上只展示了脱敏后的部分,完整数据已经出现在 Network 面板的响应体里,任何打开 DevTools 的人都能看到。
  • 调试信息遗留在生产环境console.log 打印了用户敏感数据、接口的完整返回体,构建时没有清理掉这些调试代码。

防御手段

  1. 任何"不能被别人看到"的密钥,只能放在后端环境变量里,前端代码中只应该出现"设计上就允许公开"的 key(比如某些第三方 SDK 的 public key,这类 key 本身权限受限、允许公开使用)。
  2. 生产构建关闭 sourcemap 的公开可访问性,或者只上传给错误监控平台(Sentry 等)用于内部排查,不对外部用户暴露访问路径。
  3. 接口设计上遵循"按需返回",需要脱敏的字段由后端做脱敏处理,不能依赖"前端页面上不展示"来当作安全边界——只要数据出现在响应体里,就已经泄露了。
  4. 构建流程里加一道清理步骤(比如用 babel 插件在生产构建时自动去除 console.log),并在代码审查环节留意是否有调试代码遗留。

常见误区

问题要点
认为前端代码经过压缩混淆就"看不出来",可以放心写敏感逻辑混淆只是增加阅读成本,不是加密,专门的反混淆工具或者有耐心的人依然能还原出关键逻辑;任何安全假设都不能建立在"前端代码不会被读"之上。
认为字段"前端没展示"就等于"没泄露"错,只要数据出现在了网络响应体里,就已经完成了"泄露"这个动作,是否展示在页面上只是 UI 层面的事,攻击者用 DevTools 或抓包工具能直接看到完整响应内容。
认为用了 lockfile 就完全不用担心供应链攻击lockfile 只保证"我这次安装的版本和上次一致",如果攻击者投毒的版本恰好是你第一次安装/更新依赖时锁定的那个版本,lockfile 反而会让你一直用着这个恶意版本直到手动更新,lockfile 防的是"意外升级到新的恶意版本",不是万能药,还是要配合漏洞扫描。
认为 SRI 只能用在 <script> 标签上integrity 属性同样可以用在 <link rel="stylesheet"> 引入的样式表上,CSS 同样可能被篡改用于钓鱼或样式劫持。

❌ 错误答法示范

错误示例 1:「前端不需要考虑供应链安全,那是后端和运维该管的事,前端只要把 npm install 跑起来就行。」
❌ 问题:前端项目的 node_modules 本身就是攻击面的一部分——前端依赖的 npm 包一旦被投毒,恶意代码会在用户浏览器里执行,能读取页面上所有能读到的数据(Cookie、localStorage、用户输入),危害不亚于后端供应链问题,前端团队同样需要对依赖安全负责。

错误示例 2:「postMessage 收到消息只要判断 event.data 的格式对不对就够安全了,不需要额外校验来源。」
❌ 问题:格式校验只能防"消息格式错误导致的程序崩溃",不能防"恶意页面发送了格式完全正确、但内容是伪造的消息"。攻击者完全可以构造出格式合法的消息内容,唯一能确认消息确实来自可信页面的手段是校验 event.origin,两者不能互相替代。


🎤 模拟面试作答

这三块我分开说,最后串一下它们的共同点。

postMessage 是浏览器专门用来跨窗口、跨域通信的 API,本身就是同源策略的一个官方缺口,用不好就是漏洞。两个常见的坑:一个是发送方把 targetOrigin 写成 *,如果这个窗口引用后来被跳转到了别的页面,敏感数据就发给了攻击者;另一个是接收方在 message 事件回调里不校验 event.origin,直接信任消息内容,任何页面只要拿到窗口引用都能伪造消息发过来,如果拿这个内容直接更新登录状态或者拼进 innerHTML,就可能被拿来做逻辑绕过甚至 XSS。正确做法是发送时指定明确的目标源,接收时第一步就校验来源是不是在白名单里,消息内容也要当不可信输入处理。

供应链安全说的是项目依赖的第三方包或者 CDN 资源本身被投毒,恶意代码通过"正常安装依赖"这个环节混进项目,影响面是全量用户,而且往往很隐蔽,因为开发者没有主动改任何代码。防御主要几层:用 lockfile 锁定精确版本,避免语义化版本自动升级到被投毒的新版本;定期跑 npm audit 或者接入 Snyk 这类工具扫描已知漏洞;CDN 引入的脚本加 SRI 属性,浏览器加载后会校验哈希,内容被篡改就拒绝执行;还有就是审查新增依赖,包括间接依赖,不能只看直接引入的包。

敏感信息泄露主要是几个高频场景:前端代码里硬编码密钥,这个是最不该犯的错误,因为前端代码对所有人可见,哪怕压缩混淆也只是增加阅读成本不是加密;生产环境误开 sourcemap 会暴露完整源码;接口返回了超出展示需要的字段,即使前端页面没展示,只要出现在响应体里就已经泄露了,这个不能靠前端过滤来兜底,得后端做脱敏;还有 console.log 遗留调试信息。防御上核心原则是密钥只能放后端、字段按需返回、构建时清理调试代码。

这三块放一起看有个共同的思路:都是"信任边界"的问题——postMessage 是要不要信任一个跨窗口的消息来源,供应链安全是要不要信任第三方依赖,敏感信息泄露是要不要相信"前端没展示等于安全",本质上都是不能默认信任,凡是跨越了信任边界的输入或者依赖,都要做校验或者最小化暴露。

如果面试官感兴趣,我还可以聊聊 SRI 的哈希具体是怎么生成和校验的,或者展开讲讲怎么在 CI 里搭一套自动化的依赖安全扫描流程。


❓ 面试官追问预测

追问 1:如果 iframe 是嵌入第三方支付/登录组件的场景,本来就需要跨域通信拿到支付结果,这种场景 postMessage 该怎么设计才安全?

应对思路:发送方(第三方组件)指定精确的父页面 origin,不用 *;接收方(宿主页面)校验 event.origin 必须是这个第三方组件的域名;消息内容里应该带上一个不可预测的一次性标识(比如提前生成的订单号/nonce)用来防重放,收到消息后不能只信任消息内容本身就认定支付成功,最终结果要以后端主动查询/回调验证为准,前端收到的 postMessage 更多是"提示用户去查询最新状态"的信号,不能作为唯一的信任来源。

追问 2:CDN 加了 SRI 之后,如果 CDN 服务本身宕机或者被墙,页面是不是就直接挂了?有什么兜底方案?

应对思路:是的,SRI 只解决"内容被篡改",不解决"资源不可用"。常见兜底是做多 CDN 容灾(一个 CDN 加载失败自动切换到备用 CDN 或自建的静态资源服务),或者关键依赖打进自己的构建产物里不依赖外部 CDN,用监控/资源加载失败的回调(onerror)检测并触发降级逻辑。

追问 3npm audit 报出的漏洞,是不是每次都需要立刻升级依赖处理?

应对思路:不一定,要结合实际影响评估——很多漏洞是在特定使用场景(比如仅在 Node 环境的某个 API 被调用时)才会触发,纯前端打包场景可能根本用不到那个有问题的代码路径;也要考虑升级依赖版本是否有破坏性变更(breaking change)带来的回归风险。实践中通常按漏洞的 CVSS 评分/严重程度分级处理,高危且确实命中使用路径的优先修,低风险的可以记录跟踪、排期处理,而不是每次报警都不假思索地全量升级。

追问 4:接口返回多余字段这个问题,是不是加个前端脱敏函数(比如展示前把手机号中间几位打星号)就够了?

应对思路:不够,这只解决了 UI 展示层面的问题,完整的手机号已经出现在 Network 响应体里,任何打开 DevTools 或者用抓包工具的人依然能看到明文。真正的脱敏必须在后端完成——要么接口直接返回脱敏后的数据(如果前端从不需要明文),要么针对不同权限等级返回不同粒度的数据,前端展示层的脱敏只能作为体验层面的补充,不能当成安全边界。

个人面试题归档,持续更新中