学完这篇你能弄清楚 iframe 到底把哪些攻击面引入了页面,并照着代码把防嵌套头、sandbox、postMessage 校验配好,最后在大多数场景下用更轻的方案替掉它。
第一步:先认清 iframe 带来的四类风险
iframe 本身不是漏洞,问题出在它默认“信任传递”的机制上:
- 点击劫持:攻击者用
opacity:0 的透明 iframe 覆盖在自己页面上,诱导你在不知情时点击你站点里的“删除”“转账”按钮。前提是你的站点允许被别的域名嵌套。
- 权限外溢:父页面通过
allow 属性可以把摄像头、麦克风、剪贴板、全屏等权限授予子框架,给多了等于把自己的权限借出去。
- 通信被劫持:
postMessage 如果不校验 origin,任何能把页面嵌进来的站点,甚至被注入的第三方脚本,都能往你的消息监听里塞数据,进而触发越权操作。
- 引入不可控的第三方内容:广告位、外链小工具、用户自定义嵌入的页面,会在你的页面域里执行它们的脚本、弹窗、跳转、读 referrer。
注意:iframe 里的页面不能直接读父页面同源的 DOM,但同源时它可以操作父页面,包括改写父页面的 window、导航、表单。
第二步:先给你的站点加防嵌套响应头
防点击劫持的最简单办法是禁止别人嵌套你。Nginx 站点配置里加:
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Content-Security-Policy "frame-ancestors 'self' https://partner.example.com;" always;
Apache 对应写在 .htaccess:
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Content-Security-Policy "frame-ancestors 'self'"
注意:X-Frame-Options 只认 DENY 和 SAMEORIGIN 两个值,不能列白名单域名。需要放行多个合作方,只能靠 CSP 的 frame-ancestors,现代浏览器优先读 CSP。
第三步:嵌入别人的页面时,sandbox 和 allow 要按最小权限给
<iframe
src="https://widget.example.com/card"
sandbox="allow-scripts"
allow="clipboard-write"
referrerpolicy="no-referrer"
loading="lazy"
width="600" height="360"
title="合作方卡片">
</iframe>
sandbox 默认会把表单提交、弹窗、脚本、顶层导航、同源访问全部关掉,需要哪个就在后面 allow-xxx 加哪个。
allow 控制的是浏览器特性权限(摄像头、地理位置、支付等),不写就是拒绝。
loading="lazy" 让 iframe 滚到视口附近才加载,省带宽也省首屏时间。
注意:allow-scripts 和 allow-same-origin 不要同时给同源 iframe。子页面拿到同源权限后,可以直接用 JS 把自己外层那层 sandbox 属性删掉,沙箱就失效了。这条是最常见的误配。
第四步:postMessage 必须校验来源
父页面收消息:
window.addEventListener('message', (event) => {
if (event.origin !== 'https://widget.example.com') return;
if (event.source !== iframeEl.contentWindow) return;
handle(event.data);
});
父页面发消息,第二参数写明确的目标域,不要用 '*':
iframeEl.contentWindow.postMessage({ type: 'init' }, 'https://widget.example.com');
子页面里也要校验 event.origin 是不是父站域名,并只接收约定好的 type。别把 event.data 直接丢进 innerHTML。
第五步:能不用 iframe 就别用
按场景换方案,成本比你想的低:
- 同站多页面嵌入:改为服务端模板片段或前端组件,彻底没有跨文档问题。
- 需要样式隔离:用 Shadow DOM(
attachShadow({ mode: 'open' }))而不是 iframe,隔离了 CSS 又没有跨文档通信负担。
- 想嵌入第三方内容卡片:优先用对方提供的 JSON API,自己的页面渲染成普通 DOM,脚本和样式完全可控。
- 视频/音频/地图:优先原生
<video> 或官方 JS SDK 挂载到 div,比 iframe 更省资源。
- 必须用 iframe(支付、登录、跨域富交互):至少补齐
sandbox、allow、referrerpolicy、loading="lazy",并限制单页内 iframe 数量。
第六步:上线前对照自查
- 主站响应头有
Content-Security-Policy: frame-ancestors;
- 所有外链 iframe 都带
sandbox,没有同时给两个危险开关;
allow 属性只列真正需要的权限;
postMessage 收发两端都校验 origin,发送端不用 '*';
- 页面内 iframe 数量与位置可控,第三方内容有降级兜底。
小结
- iframe 的风险来自“默认继承信任”,而不是标签本身。
- 防嵌套靠响应头:
X-Frame-Options 粗粒度,frame-ancestors 能列白名单。
- 嵌外部页面用
sandbox + allow + referrerpolicy 做最小权限,allow-scripts 与 allow-same-origin 别同开。
postMessage 收发都要校验来源,发送指定具体域名,别用通配。
- 同站嵌入、样式隔离、内容卡片这三类需求,优先用组件、Shadow DOM、JSON API 替代 iframe。