🔥千万别让 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 开始。