为什么设置了X-Frame-Options后页面还是被嵌入到第三方iframe了?

Prog.书瑜 阅读 70

我在项目里加了X-Frame-Options: DENY和CSP的frame-ancestors 'none',但测试时发现页面还是能被其他网站用透明iframe嵌套住。点击他们页面的按钮时,居然触发了我页面里的隐藏删除按钮。明明配置了这些防护,为什么会失效?

尝试过在服务端设置HTTP头:


Content-Security-Policy: frame-ancestors 'none'
X-Frame-Options: DENY

同时页面里也加了meta标签:


<meta http-equiv="X-Frame-Options" content="DENY">

但用第三方测试页面(代码见下方)还是能嵌套成功…

测试用的嵌套页面代码:


<div style="pointer-events:none; width:200px; height:200px">
  <iframe src="https://我的网站.com" style="opacity:0; width:200px; height:200px"></iframe>
  <button style="position:absolute">点击领取奖励</button>
</div>

难道是服务器配置没生效?或者还需要其他防护措施?

我来解答 赞 15 收藏
二维码
手机扫码查看
2 条解答
西门义霞
首先你要明白 X-Frame-Options 和 CSP 的 frame-ancestors 是用来防止你的页面被嵌入 iframe 的,但它们只能在浏览器加载你页面的时候起作用。也就是说,如果第三方网站用 iframe 嵌套了你的页面,而你的服务器确实正确返回了 X-Frame-Options: DENYContent-Security-Policy: frame-ancestors 'none',那这个 iframe 应该根本加载不出来,直接被浏览器拦截。

但现在你说它“还是能嵌入”,而且还能通过一个透明 iframe 触发你页面里的按钮——这说明最可能的情况是:你的 HTTP 头根本没有生效。

我们一步步排查和解决:

第一步,确认响应头是否真的发出去了

打开浏览器开发者工具(F12),切换到 Network 选项卡,刷新你的页面,找到最开始的 HTML 文档请求(一般是 document 类型),点进去看 Response Headers。确认里面有没有:

Content-Security-Policy: frame-ancestors 'none';
X-Frame-Options: DENY

如果你没看到这两个头,或者只有其中一个,那就说明服务端配置有问题。注意:HTTP 头优先级高于 meta 标签,而 X-Frame-Options 用 meta 标签的方式只在部分旧浏览器支持,现代浏览器主要看 HTTP 响应头。

常见错误是:
- 在 Nginx/Apache 配置里写了,但没 reload
- 代码里设置 header 的地方被后面的逻辑覆盖了
- 使用了 CDN 或反向代理,实际请求没走到你的服务

举个 Nginx 正确配置的例子:

server {
listen 443 ssl;
server_name 你的网站.com;

# 关键在这里:添加安全头
add_header Content-Security-Policy "frame-ancestors 'none';" always;
add_header X-Frame-Options DENY always;

location / {
proxy_pass http://your-backend;
# 注意:如果后端自己也设置了这些头,别让 proxy_hide_header 把它吃掉了
}
}


always 是为了让它对所有响应类型(包括 4xx/5xx)都生效。

第二步,检查有没有被其他中间件或框架覆盖

比如你在 Node.js + Express 里这么写:

app.use((req, res, next) => {
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('Content-Security-Policy', "frame-ancestors 'none'");
next();
});

但如果后面某个路由又调用了 setHeader,可能会被覆盖。应该确保这些头是在最终输出前设置的,并且不要用 writeHead 之类的低级方法绕过。

更稳妥的做法是在最后统一设置:

// Express 示例
app.use((req, res, next) => {
// 其他逻辑...
next();
});

// 放在所有中间件之后,确保不被覆盖
app.use((req, res, next) => {
if (!res.get('X-Frame-Options')) {
res.setHeader('X-Frame-Options', 'DENY');
}
if (!res.get('Content-Security-Policy')) {
res.setHeader('Content-Security-Policy', "frame-ancestors 'none';");
}
next();
});


第三步,理解攻击原理:你遇到的是“点击劫持”(Clickjacking)

虽然你说加了防护,但从描述来看,iframe 能加载出来,说明防护完全没生效。现在对方页面用了一个透明 iframe 叠在自己的按钮上面,用户以为点的是“领取奖励”,实际上点的是你页面里位置对应的删除按钮。

这种攻击不需要 JS 交互,纯靠 DOM 层叠就能完成。所以仅靠前端 JS 检测 window.top !== window 是不够的,必须从源头阻止嵌套。

第四步,为什么 meta 标签不起作用?

这种写法其实是模拟 HTTP 头,但很多浏览器根本不支持这种用法,尤其是现代浏览器。X-Frame-Options 必须作为 HTTP 响应头发送才可靠。CSP 的 meta 标签支持好一些,但也有限制,比如不能用于 worker 等上下文。

所以结论是:别依赖 meta 标签做这类安全防护。

第五步,加强防御措施

即使头设置正确,为了双重保险,建议加上 JavaScript 防嵌套检测:

// 放在页面最前面,防止被绕过
if (window.top !== window.self) {
// 立即跳出 iframe
document.documentElement.style.display = 'none';
window.top.location = window.self.location;
}


这段代码的作用是:一旦发现页面不是在顶层窗口打开,就隐藏当前页面内容,并尝试跳转顶层窗口到你的页面地址,破坏攻击者的布局。

注意要放在 最前面,避免攻击者在你加载完之前完成点击。

第六步,验证是否真正生效

你可以写一个本地测试页:

<iframe src="https://你的网站.com" style="width:300px;height:300px"></iframe>


如果一切正常,这个 iframe 应该显示为空白或报错(Chrome 控制台会提示 Refused to display in a frame because an ancestor violates the following Content Security Policy directive)。

最后提醒一点:CSP 的 frame-ancestors 会覆盖 X-Frame-Options。也就是说,只要 CSP 生效了,浏览器就会忽略 X-Frame-Options。所以理论上你只需要配好 CSP 就行,但为了兼容老浏览器(比如 IE),建议两个都保留。

总结一下你应该做的事:

1. 检查真实响应头是否包含 X-Frame-Options: DENY 和 Content-Security-Policy: frame-ancestors 'none'
2. 如果没有,去服务端或 CDN 配置处补上,确保头被正确发送
3. 删除无用的 meta 标签,改用 HTTP 头
4. 加上 JS 防嵌套脚本作为后备
5. 用外部页面测试是否还能被嵌入

做完这些,基本就能防住绝大多数点击劫持攻击了。你现在的问题大概率就是头没发出去,导致整个防护形同虚设。
点赞 5
2026-02-11 12:09
柯汝
柯汝 Lv1
这个问题挺有意思的,确实是个常见的安全问题。你设置了 X-Frame-OptionsContent-Security-Policy,理论上是可以防止页面被嵌套的,但还是被绕过了。原因其实很简单,第三方用了一个“透明iframe + 遮罩”的方式来伪装点击行为。下面我们一步步来分析问题并解决。

---

### 1. **问题的本质**
第三方通过设置 opacity:0 把你的页面变成了透明的,并且用 pointer-events:none 让用户的鼠标事件穿透到 iframe 上。这样一来,用户点击了看起来是“领取奖励”的按钮,实际上却触发了你页面里的隐藏删除按钮。这种攻击叫做“点击劫持”(Clickjacking)。

虽然你设置了 X-Frame-Optionsframe-ancestors,但如果浏览器不支持这些头(比如老旧浏览器),或者某些配置没生效,就可能被绕过。

---

### 2. **确认服务器端配置是否生效**
首先检查一下服务器返回的 HTTP 响应头是否正确:

curl -I https://我的网站.com


你应该能看到类似这样的响应头:

HTTP/1.1 200 OK
Content-Security-Policy: frame-ancestors 'none'
X-Frame-Options: DENY


如果看不到这些头,说明服务器配置有问题。以下是一些常见服务器的配置方法:

#### Nginx

server {
add_header Content-Security-Policy "frame-ancestors 'none'";
add_header X-Frame-Options DENY;
}


#### Apache

Header always set Content-Security-Policy "frame-ancestors 'none'"
Header always set X-Frame-Options DENY


如果服务器配置没问题,但还是能被嵌套,那可能是浏览器兼容性的问题,或者有其他地方覆盖了这些头。

---

### 3. **前端额外防护措施**
即使服务器配置正常,为了防止老版本浏览器或者其他意外情况,建议在前端也加上防护代码。这里推荐使用 JavaScript 的 framekiller 方法。

在页面加载时,检测当前页面是否被嵌套在一个 iframe 中。如果是,强制跳转到顶层窗口:


// 检查是否被嵌套
if (window.top !== window.self) {
// 如果被嵌套,尝试跳出 iframe
try {
window.top.location = window.self.location;
} catch (e) {
// 如果跳出失败,显示一个警告页面
document.body.innerHTML = '<p>该页面不允许被嵌套,请访问原始页面:<a href="' + window.self.location.href + '">点击这里</a></p>';
}
}


这段代码的作用是:
1. 检查当前页面是否在顶层窗口中。
2. 如果不是,尝试把顶层窗口重定向到当前页面。
3. 如果重定向失败(可能因为跨域限制),直接替换页面内容为一个提示信息。

---

### 4. **防止点击劫持**
除了防止页面被嵌套,还要防止类似的点击劫持攻击。你可以通过 CSS 来禁用页面上的透明元素或遮罩层。例如,在全局样式中添加以下规则:

/* 禁止任何透明元素 */
*:not(canvas):not(img) {
opacity: initial !important;
pointer-events: initial !important;
}


这段代码会强制所有非图片、非画布的元素恢复正常的透明度和鼠标事件,避免被恶意利用。

---

### 5. **测试和完善**
做完以上步骤后,再次用第三方测试页面试试看。如果还有问题,可以逐步排查以下几点:
- 浏览器是否支持 Content-Security-PolicyX-Frame-Options
- 是否有其他中间代理(如 CDN)修改了响应头。
- 是否有其他脚本动态移除了防护代码。

---

### 总结
你的配置本身是对的,但点击劫持攻击可以通过一些技巧绕过。建议从服务器端和前端同时加强防护,确保万无一失。如果还有其他问题,随时可以继续讨论!
点赞 10
2026-02-01 08:05