千万别让 iframe“裸奔”!黑客用它3秒盗走用户 Cookie,我是这样堵死的

2 阅读7分钟

🔥千万别让 iframe“裸奔”!黑客用它3秒盗走用户 Cookie,我是这样堵死的。

如果你也在用 Vue2(或任何前端框架)做类似的文件预览功能,请花几分钟看完这场“安全保卫战”的真实复盘——以及我们如何用几道防线把风险彻底锁死。 如果有一天,你做的资源预览功能突然弹出博彩广告,用户数据被悄无声息地盗走,而源头仅仅是一个看似无害的文件链接——你会不会后背发凉?

这不是演习。就在上周,我们刚上线的资源库差点沦为黑客的“自助提款机”。用户把网络文件地址粘贴进来,系统用 <iframe> 老实巴交地打开,结果恶意脚本长驱直入:跳转钓鱼页面、窃取 Cookie、弹出满屏不堪入目的广告……整个过程不到 3 秒。

我连忙救火开展修复,把这颗“定时炸弹”拆掉。今天这篇复盘,就是把血泪教训变成你的防御铠甲——哪怕你只来得及给 <iframe> 加一个属性,都能把 90% 的攻击拦在门外。

凌晨两点,屏幕上的告警邮件把我从梦里拽了出来:“用户页面弹出大量色情广告,疑似被植入恶意脚本。”我心里咯噔一下——那个刚刚上线的资源库功能,允许用户输入网络文件地址,然后用一个平平无奇的 <iframe> 进行预览。难道……

我颤抖着点开用户提交的链接,页面里竟然明目张胆地挂着:

html

<script>
  window.top.location.href = 'http://钓鱼网站.com';
  // 再偷走你的 cookie……
</script>

就这么几行代码,我们的平台差点变成攻击者的跳板,用户的账号、数据危在旦夕。那一夜,我深刻理解了一句话:你永远不知道用户在 iframe 里塞了什么,但黑客永远知道怎么利用它。

如果你也在用 Vue2(或任何前端框架)做类似的文件预览功能,请花几分钟看完这场“安全保卫战”的真实复盘——以及我们如何用几道防线把风险彻底锁死。


1. 祸根:一个“敞开大门”的 iframe

起初的代码简单到令人发指:

vue

<template>
  <iframe :src="fileUrl" frameborder="0" width="100%" height="500px"></iframe>
</template>

<script>
export default {
  data() {
    return {
      fileUrl: '' // 用户输入的任意 URL
    }
  }
}
</script>

用户粘贴一个 https://evil.com/steal.html,iframe 就会原封不动地加载它。此时,恶意页面拥有和普通网页几乎一样的权限:执行脚本、读取某些存储、甚至尝试跳转父页面——同源策略虽然拦了一部分,但 iframe 本身就是一个完整的浏览器上下文,足以发动钓鱼、挖矿、弹窗等各种攻击。

更糟的是,如果用户输入的不是 http://,而是 javascript:alert(document.cookie) 这类伪协议,某些浏览器可能会直接在当前页面上下文执行脚本,连 iframe 的隔离都形同虚设。


2. 加固第一线:给你的 iframe 套上“枷锁”

iframe 有一个常常被忽视的属性——sandbox。它可以像监狱一样,限制内部页面的一举一动。这是性价比最高、也是最直接的第一道防线。

我们立刻修改了模板:

vue

<iframe
  :src="safeFileUrl"
  sandbox="allow-same-origin"
  frameborder="0"
  width="100%"
  height="500px"
></iframe>

几个常用的权限标记组合:

  • sandbox="" (最严格):禁止任何脚本、表单提交、弹窗、插件等。页面几乎是“只读”的静态快照。
  • sandbox="allow-same-origin" :允许访问同源存储和 Cookie,但仍然禁止脚本执行(除非同时添加 allow-scripts)。
  • sandbox="allow-scripts allow-same-origin" :允许脚本执行且拥有同源权限——这几乎等于没加沙箱,除非你完全信任内容,否则绝对不要一起用。

对于大多数文件预览(图片、PDF、纯文本),我们不需要脚本。即便预览的是 HTML 文档,我们也应假设它不可信,默认禁止脚本。如果业务必须支持交互式页面,那就需要在安全与功能间取舍,但永远记住:最小权限原则,只给必需的能力。

加上 sandbox="allow-same-origin" 后,之前那个恶意脚本直接哑火,广告弹窗消失得无影无踪。


3. 加固第二线:给 URL 装一道“安检门”

沙箱可以束缚已加载的恶意页面,但我们能不能从源头就把可疑地址拒之门外?当然可以——在数据进入 :src 之前,用 Vue2 的计算属性或侦听器做一次严格的过滤。

javascript

computed: {
  safeFileUrl() {
    let url = this.fileUrl.trim()
    if (!url) return ''

    // 1. 阻断 javascript: / data: 等危险伪协议
    const dangerousProtocols = /^(javascript|data|vbscript):/i
    if (dangerousProtocols.test(url)) {
      console.warn('危险协议已被拦截:', url)
      return ''
    }

    // 2. 只允许 http/https 协议
    const safeProtocols = /^https?:///i
    if (!safeProtocols.test(url)) {
      // 可以尝试自动补全 https://,但要警惕用户刻意构造
      url = 'https://' + url
    }

    // 3. 域名白名单(强烈推荐)
    const allowedDomains = [
      'example.com',
      'cdn.mycompany.com',
      'docs.google.com'
    ]
    try {
      const hostname = new URL(url).hostname
      const isAllowed = allowedDomains.some(domain =>
        hostname === domain || hostname.endsWith('.' + domain)
      )
      if (!isAllowed) {
        console.warn('域名不在白名单中:', hostname)
        return ''
      }
    } catch (e) {
      // URL 格式非法,直接丢弃
      return ''
    }

    return url
  }
}

这样,任何企图通过 javascript: 伪协议或未知域名的请求,都会在源头被掐灭。白名单可以根据你的资源库来源动态调整,比如只允许公司内部 CDN 或已审核的第三方站点。


4. 加固第三线:内容安全策略(CSP)——最后一道“天网”

即使 iframe 被沙箱限制,即使 URL 经过了白名单校验,我们仍然可以在 HTTP 响应头(或 HTML meta 标签)中再加一层保险。内容安全策略(CSP)能够告诉浏览器:“只允许从这些地方加载脚本、只允许嵌入这些来源的 frame”。

在后端返回 HTML 时加上响应头:

text

Content-Security-Policy:
  default-src 'none';
  frame-src https://trusted.com https://cdn.mycompany.com;
  script-src 'none';
  object-src 'none';

或者在 Vue2 的 index.html 中使用 meta 标签(仅对当前页面生效):

html

<meta http-equiv="Content-Security-Policy"
      content="default-src 'none'; frame-src https://trusted.com; script-src 'none';">

这样,即便前端过滤被绕过,或者某个被允许的域名突然被黑,CSP 还能在浏览器层面强制执行白名单,阻断第三方恶意脚本的加载。frame-src 指令直接控制哪些 URL 可以被嵌入 iframe,完美契合我们的场景。


5. 额外彩蛋:服务端代理,化“毒”为“药”

对于一些必须预览但又不太受信任的 HTML 内容,我们还可以走服务端代理渲染。思路是:后端抓取目标 URL 的内容,在服务端清洗掉所有 <script><iframe>onerror 等危险标签和事件属性,然后将干净的 HTML 返回给前端,再用 srcdoc 属性直接嵌入 iframe。

vue

<iframe
  :srcdoc="sanitizedHtml"
  sandbox="allow-same-origin"
  width="100%"
  height="500px"
></iframe>

后端(例如 Node.js)可以使用 DOMPurify 这样的库(有服务端版本)来清洗 HTML。这样既能展示富文本内容,又彻底消灭了脚本执行的可能性。


6. 写在最后:安全是“活着的城墙”

修复完成后,我重新合上电脑,天已经蒙蒙亮。这次攻击没有造成用户数据泄露,但给我上了沉重的一课:任何接收用户输入并展示的地方,都是攻击面。  iframe 看似只是“一个窗口”,但如果毫无防备,它就是穿透你整个应用的一扇大门。

总结一下我们的防御组合拳:

  • iframe sandbox 属性——禁脚本、禁弹窗,最小权限。
  • 前端 URL 过滤——拦截伪协议、实施域名白名单。
  • 内容安全策略(CSP) ——浏览器级别的最后兜底。
  • 服务端内容净化——高风险场景下的终极清洗。

这四道防线层层递进,单点失效也不会导致全线崩溃。如今那个资源库功能依然平稳运行,每天处理数万次预览,再也没有出现过安全告警。

如果你正在开发类似的功能,千万不要像我一样,等到被黑才醒悟。现在就打开你的代码,检查每一个 <iframe>——给它上把锁,真的不费事。

安全开发,从敬畏每一个 :src 开始。