点击劫持攻击完整防护指南
本文从小白角度全面讲解点击劫持攻击(Clickjacking)的原理、攻击方式、真实案例,以及详细的防护措施。涵盖 X-Frame-Options、Content Security Policy、JavaScript 防护等多种方法,并提供实际代码示例和最佳实践。
目录
- 什么是点击劫持攻击?
- 点击劫持是如何工作的?
- 为什么点击劫持很危险?
- 真实攻击案例
- 如何检测点击劫持攻击?
- 防护措施一:X-Frame-Options 响应头
- 防护措施二:Content Security Policy(CSP)
- 防护措施三:JavaScript 防护(帧破坏)
- 防护措施四:SameSite Cookie 属性
- 完整防护方案(推荐)
- 前端开发者的防护责任
- 常见问题与最佳实践
什么是点击劫持攻击?
简单理解
想象一下,你看到一个好看的按钮写着"点击领取奖品",你以为点击的是这个按钮,但实际上,你的点击被"偷偷"转发到了另一个隐藏在下面的按钮上。这就是点击劫持攻击(Clickjacking)。
点击劫持,也被称为UI 覆盖攻击(UI Redressing Attack),是一种恶意网站将透明的 iframe 叠加在另一个网页上,诱骗用户点击他们以为是无害按钮的技术。
专业定义
点击劫持是一种视觉欺骗攻击,攻击者在一个网页上覆盖一个透明的 iframe,这个 iframe 加载了目标网站(通常是受信任的网站)。用户在点击看似正常的页面元素时,实际上点击的是隐藏在下面的目标网站的操作按钮。
可视化理解
┌─────────────────────────────────┐
│ 恶意的诱骗页面(攻击者控制) │
│ │
│ [看起来很正常的按钮] │
│ "点击领取奖品" │
│ │
│ ╔═══════════════════════════╗ │
│ ║ 透明 iframe(看不见) ║ │
│ ║ 加载了目标网站 ║ │
│ ║ 例如:银行转账确认按钮 ║ │
│ ╚═══════════════════════════╝ │
│ │
└─────────────────────────────────┘用户以为点击的是"领取奖品"
实际上点击的是"确认转账"按钮
点击劫持是如何工作的?
攻击流程步骤详解
第一步:创建恶意页面
攻击者创建一个看起来正常、吸引人的网页:
<!DOCTYPE html>
<html>
<head><title>免费领取大奖!</title><style>.fake-button {position: absolute;top: 200px;left: 200px;width: 200px;height: 50px;background: linear-gradient(45deg, #ff6b6b, #4ecdc4);color: white;border: none;border-radius: 10px;font-size: 18px;cursor: pointer;z-index: 2;}/* 透明 iframe,完全覆盖在按钮上方 */.hidden-iframe {position: absolute;top: 200px;left: 200px;width: 200px;height: 50px;opacity: 0; /* 完全透明 */z-index: 3; /* 在按钮上方 */pointer-events: all;}</style>
</head>
<body><h1>🎉 恭喜您!您获得了价值 10000 元的大奖!</h1><p>只需点击下方按钮即可领取:</p><!-- 假的按钮(诱饵) --><button class="fake-button">立即领取大奖</button><!-- 透明的 iframe,加载目标网站 --><iframe class="hidden-iframe"src="https://bank.example.com/transfer?to=hacker&amount=10000"></iframe><script>// 当用户点击时,iframe 会捕获这个点击事件// 如果目标网站没有防护,用户的点击会被转发到 iframe 内的按钮</script>
</body>
</html>
第二步:精确定位覆盖
攻击者使用 CSS 让 iframe 完全覆盖在诱骗按钮上:
/* 关键技术:绝对定位 + 透明度 + z-index */
.hidden-iframe {position: absolute; /* 绝对定位 */opacity: 0; /* 完全透明,用户看不见 */z-index: 9999; /* 确保在最上层 */pointer-events: all; /* 确保可以接收点击事件 */
}
第三步:用户被欺骗
- 用户访问恶意网页
- 看到诱人的"领取大奖"按钮
- 点击按钮
- 实际上点击的是透明的 iframe
- iframe 内的目标网站按钮被触发
- 攻击成功!
更复杂的攻击场景
场景一:社交媒体点赞劫持
<!-- 攻击者页面 -->
<div style="position: relative;"><h2>看这个有趣的视频!</h2><!-- 诱骗用户点击的区域 --><div class="fake-content"><img src="cute-cat.jpg" alt="可爱的猫咪"></div><!-- 透明 iframe 覆盖在图片上 --><iframe src="https://social-media.com/post/123/like"style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; opacity: 0; z-index: 9999;"></iframe>
</div>
用户点击图片以为在看视频,实际触发了"点赞"操作。
场景二:银行转账劫持
<!-- 攻击者页面 -->
<div class="game-container"><h1>🐍 贪吃蛇小游戏</h1><p>点击开始游戏按钮:</p><button class="start-button">开始游戏</button><!-- 透明 iframe,加载银行转账页面 --><iframe src="https://bank.com/transfer?to=hacker_account&amount=5000"style="position: absolute; top: 150px; left: 50%; transform: translateX(-50%);width: 400px; height: 300px; opacity: 0; z-index: 9999;"></iframe>
</div><script>
// 更精确的控制:调整 iframe 位置,让它覆盖转账确认按钮
setTimeout(() => {const iframe = document.querySelector('iframe');// 动态调整位置,精确覆盖目标按钮iframe.style.left = '400px';iframe.style.top = '500px';
}, 1000);
</script>
为什么点击劫持很危险?
1. 用户完全不知情
用户认为自己点击的是正常的按钮,但实际上:
- 可能触发了银行转账
- 可能更改了账户设置
- 可能发布了不当内容
- 可能点赞/关注了不想要的账号
- 可能授权了应用访问权限
2. 结合社会工程学
点击劫持经常与其他攻击结合:
用户收到邮件:
"您的账户异常,请立即点击这里验证"→ 跳转到恶意页面
→ 看起来像是银行的登录页面
→ 用户输入密码
→ 密码被窃取 + 同时触发账户设置更改(通过点击劫持)
3. 绕过传统安全检查
- ✅ 目标网站有 HTTPS 加密
- ✅ 用户已登录(有有效的 Cookie)
- ✅ 有 CSRF Token 保护
- ❌ 但没有防止在 iframe 中加载
结果:攻击者利用用户已建立的信任关系进行攻击。
4. 难以追溯
用户不知道自己被攻击了:
- 没有明显的异常
- 攻击发生在正常的网页交互中
- 难以向用户解释发生了什么
真实攻击案例
案例一:Twitter 点击劫持(2009年)
攻击方式:
- 攻击者创建了一个"TwitPorn"页面
- 页面显示"点击按钮看图片"
- 透明 iframe 加载了 Twitter 的关注按钮
- 用户点击后,自动关注了攻击者的账号
影响:
- 大量用户被诱骗关注恶意账号
- Twitter 发现后紧急修复,推出了防护措施
案例二:Facebook Like 劫持(2010年)
攻击方式:
- 恶意页面显示诱人的内容(如"点击看美女照片")
- 透明 iframe 覆盖在内容上
- iframe 加载了 Facebook 的 Like 按钮
- 用户点击后,自动点赞了恶意页面
影响:
- 恶意内容通过病毒式传播获得大量点赞
- 用户的好友看到这些内容,形成链式反应
案例三:银行转账劫持(2013年)
攻击方式:
- 钓鱼邮件声称"账户需要验证"
- 点击链接进入"银行安全页面"
- 页面看起来正常,让用户放心
- 透明 iframe 覆盖在"继续"按钮上
- iframe 中加载了真实的银行转账确认页面
- 用户点击"继续",实际确认了一笔转账
影响:
- 用户资金被盗
- 银行声誉受损
案例四:Adobe Flash 点击劫持(2015年)
攻击方式:
- 利用 Flash 的跨域特性
- 创建透明 Flash 覆盖层
- 即使用户禁止了 iframe,Flash 仍然可以工作
- 劫持用户的点击操作
如何检测点击劫持攻击?
方法一:作为普通用户检测
1. 检查是否可以选中文字
在可疑页面上尝试选中文字:
- 如果可以选中 → 可能是正常页面
- 如果不能选中 → 可能有透明层覆盖
2. 检查鼠标悬停效果
将鼠标悬停在按钮上:
- 查看浏览器状态栏显示的链接地址
- 如果不一致,可能是攻击页面
3. 使用浏览器开发者工具
按 F12 打开开发者工具
→ 点击 Elements 标签
→ 查看是否有多个重叠的元素
→ 检查 iframe 标签
4. 检查页面源代码
右键 → 查看页面源代码:
- 搜索
<iframe>标签 - 如果发现可疑的透明 iframe,可能是攻击
方法二:作为开发者检测自己的网站
1. 检查响应头
# 使用 curl 检查响应头
curl -I https://your-website.com# 查看是否有 X-Frame-Options 或 CSP 头部
没有防护的网站:
HTTP/1.1 200 OK
Content-Type: text/html
...
有防护的网站:
HTTP/1.1 200 OK
Content-Type: text/html
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'
...
2. 尝试在自己的页面中嵌入 iframe
创建一个测试页面:
<!DOCTYPE html>
<html>
<head><title>点击劫持测试</title>
</head>
<body><h1>测试:尝试将目标网站嵌入 iframe</h1><!-- 如果可以嵌入,说明没有防护 --><iframe src="https://your-website.com" width="100%" height="600px"></iframe><p>如果你能看到上面的内容,说明目标网站没有防护点击劫持。</p>
</body>
</html>
- 如果可以正常显示 → ❌ 没有防护
- 如果显示空白或被阻止 → ✅ 有防护
3. 使用在线检测工具
- Clickjacking Test Tool
- Security Headers
防护措施一:X-Frame-Options 响应头
什么是 X-Frame-Options?
X-Frame-Options 是 HTTP 响应头,告诉浏览器是否允许将页面嵌入到 <iframe>、<frame>、<embed> 或 <object> 中。
三种取值
1. DENY - 完全禁止
X-Frame-Options: DENY
效果:
- 页面不能在任何情况下被嵌入 iframe
- 最安全,但也最严格
- 即使用户想在自己的网站中嵌入也不可以
适用场景:
- 银行网站
- 支付页面
- 敏感操作页面
示例代码(Node.js/Express):
app.use((req, res, next) => {res.setHeader('X-Frame-Options', 'DENY');next();
});
2. SAMEORIGIN - 同源允许
X-Frame-Options: SAMEORIGIN
效果:
- 只允许同源(同一个域名)的页面嵌入
- 其他域名的页面不能嵌入
- 平衡了安全性和灵活性
适用场景:
- 大多数网站
- 需要在自己网站内嵌页面的场景
- 推荐使用
示例代码(Node.js/Express):
app.use((req, res, next) => {res.setHeader('X-Frame-Options', 'SAMEORIGIN');next();
});
3. ALLOW-FROM uri - 指定来源允许
⚠️ 注意:此选项已被废弃,不推荐使用
旧版浏览器支持,但现代浏览器(Chrome、Firefox)已不支持。
完整的服务器配置示例
Node.js/Express
const express = require('express');
const app = express();// 全局设置 X-Frame-Options
app.use((req, res, next) => {// 对于敏感页面,使用 DENYif (req.path.startsWith('/payment') || req.path.startsWith('/transfer')) {res.setHeader('X-Frame-Options', 'DENY');} // 对于普通页面,使用 SAMEORIGINelse {res.setHeader('X-Frame-Options', 'SAMEORIGIN');}next();
});app.listen(3000);
Nginx
# 全局设置
add_header X-Frame-Options "SAMEORIGIN" always;# 或者针对特定路径
location /payment {add_header X-Frame-Options "DENY" always;
}
Apache
# .htaccess 文件
Header always set X-Frame-Options "SAMEORIGIN"# 或者针对特定路径
<LocationMatch "^/payment">Header always set X-Frame-Options "DENY"
</LocationMatch>
PHP
<?php
// 在 PHP 文件中设置
header('X-Frame-Options: SAMEORIGIN');// 或者针对不同页面
if (strpos($_SERVER['REQUEST_URI'], '/payment') !== false) {header('X-Frame-Options: DENY');
} else {header('X-Frame-Options: SAMEORIGIN');
}
?>
验证是否生效
// 在前端测试
fetch('https://your-website.com', { method: 'HEAD' }).then(response => {console.log('X-Frame-Options:', response.headers.get('X-Frame-Options'));});
# 使用 curl 验证
curl -I https://your-website.com | grep -i "x-frame-options"
防护措施二:Content Security Policy(CSP)
什么是 CSP?
Content Security Policy(内容安全策略)是一个强大的安全机制,不仅可以防护点击劫持,还可以防护 XSS、数据注入等多种攻击。
CSP frame-ancestors 指令
专门用于防护点击劫持的 CSP 指令是 frame-ancestors。
frame-ancestors 的值
1. frame-ancestors 'none' - 完全禁止
Content-Security-Policy: frame-ancestors 'none'
等同于 X-Frame-Options: DENY
示例:
app.use((req, res, next) => {res.setHeader("Content-Security-Policy", "frame-ancestors 'none'");next();
});
2. frame-ancestors 'self' - 同源允许
Content-Security-Policy: frame-ancestors 'self'
等同于 X-Frame-Options: SAMEORIGIN
示例:
app.use((req, res, next) => {res.setHeader("Content-Security-Policy", "frame-ancestors 'self'");next();
});
3. frame-ancestors https://example.com - 指定域名
Content-Security-Policy: frame-ancestors 'self' https://trusted-site.com
优势:
- 可以指定多个允许的域名
- 比
X-Frame-Options更灵活
示例:
app.use((req, res, next) => {// 允许同源和特定域名嵌入res.setHeader("Content-Security-Policy", "frame-ancestors 'self' https://dashboard.example.com https://admin.example.com");next();
});
完整的 CSP 配置示例
CSP 不仅可以防护点击劫持,还可以防护其他攻击:
app.use((req, res, next) => {const cspHeader = ["default-src 'self'", // 默认只允许同源资源"script-src 'self' 'unsafe-inline'", // 允许同源脚本和内联脚本"style-src 'self' 'unsafe-inline'", // 允许同源样式和内联样式"img-src 'self' data: https:", // 允许同源图片、data URI 和 HTTPS 图片"font-src 'self'", // 只允许同源字体"frame-ancestors 'self'", // 防护点击劫持:只允许同源嵌入"base-uri 'self'", // 只允许同源的 base URI].join('; ');res.setHeader("Content-Security-Policy", cspHeader);next();
});
X-Frame-Options vs CSP frame-ancestors
| 特性 | X-Frame-Options | CSP frame-ancestors |
|---|---|---|
| 浏览器支持 | 所有现代浏览器 | 所有现代浏览器 |
| 灵活性 | 较简单 | 更灵活(可指定多个域名) |
| 功能范围 | 只防护点击劫持 | 可防护多种攻击 |
| 推荐度 | 基础防护 | 推荐使用(更现代) |
最佳实践:同时使用两者
app.use((req, res, next) => {// CSP(现代标准,推荐)res.setHeader("Content-Security-Policy", "frame-ancestors 'self'");// X-Frame-Options(向后兼容旧浏览器)res.setHeader("X-Frame-Options", "SAMEORIGIN");next();
});
为什么同时使用?
- CSP
frame-ancestors是现代标准,功能更强大 X-Frame-Options确保旧浏览器也能得到保护
防护措施三:JavaScript 防护(帧破坏)
什么是帧破坏(Frame Busting)?
帧破坏是一种客户端 JavaScript 技术,检测页面是否被嵌入到 iframe 中,如果是,则尝试"破坏"框架(跳转到顶层窗口)。
基础帧破坏代码
// 方法一:检查 window.top 和 window.self
if (window.top !== window.self) {// 页面被嵌入到 iframe 中window.top.location = window.self.location;// 强制跳转到当前页面(打破框架)
}
更强大的帧破坏代码
完整版本(推荐)
(function() {'use strict';// 检查是否被嵌入if (window.top !== window.self) {// 尝试打破框架try {// 方法1:直接跳转window.top.location = window.self.location;} catch (e) {// 方法2:如果被阻止,创建覆盖整个页面的警告document.body.innerHTML = '';const warning = document.createElement('div');warning.style.cssText = `position: fixed;top: 0;left: 0;width: 100%;height: 100%;background: #ff4444;color: white;display: flex;align-items: center;justify-content: center;font-size: 24px;z-index: 999999;text-align: center;padding: 20px;box-sizing: border-box;`;warning.innerHTML = `<div><h1>⚠️ 安全警告</h1><p>此页面不能嵌入到其他网页中。</p><p>请在新窗口中打开:</p><a href="${window.location.href}" style="color: white; text-decoration: underline; font-size: 20px;"target="_blank">点击这里打开</a></div>`;document.body.appendChild(warning);}}
})();
增强版本(支持更多场景)
(function() {'use strict';// 多重检测const isFramed = window.top !== window.self || window.frameElement !== null ||window.parent !== window.self;if (isFramed) {// 尝试打破框架try {// 直接跳转(最直接的方法)window.top.location = window.self.location;} catch (e1) {try {// 使用 parentwindow.parent.location = window.self.location;} catch (e2) {// 如果都被阻止,显示警告页面document.documentElement.innerHTML = `<!DOCTYPE html><html><head><title>安全警告</title><meta charset="utf-8"><style>body {margin: 0;padding: 0;background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;display: flex;align-items: center;justify-content: center;min-height: 100vh;color: white;}.container {text-align: center;padding: 40px;background: rgba(255, 255, 255, 0.1);border-radius: 20px;backdrop-filter: blur(10px);max-width: 600px;}h1 { font-size: 48px; margin: 0 0 20px 0; }p { font-size: 18px; line-height: 1.6; margin: 20px 0; }a {display: inline-block;margin-top: 20px;padding: 15px 30px;background: white;color: #667eea;text-decoration: none;border-radius: 10px;font-weight: bold;font-size: 16px;transition: transform 0.2s;}a:hover { transform: scale(1.05); }</style></head><body><div class="container"><h1>⚠️</h1><h2>安全警告</h2><p>此页面出于安全考虑,不能嵌入到其他网页中。</p><p>请在浏览器的新标签页中打开此页面。</p><a href="${window.location.href}" target="_blank">在新窗口中打开</a></div></body></html>`;}}}
})();
在 HTML 中使用
方式一:内联脚本(快速生效)
<!DOCTYPE html>
<html>
<head><title>受保护的页面</title><script>// 立即执行,在页面加载前就检测if (window.top !== window.self) {window.top.location = window.self.location;}</script>
</head>
<body><!-- 页面内容 -->
</body>
</html>
方式二:外部脚本
<!DOCTYPE html>
<html>
<head><title>受保护的页面</title><!-- 在 head 中立即加载 --><script src="/js/frame-busting.js"></script>
</head>
<body><!-- 页面内容 -->
</body>
</html>
帧破坏的局限性
⚠️ 重要:帧破坏不是完美的防护措施
1. JavaScript 可以被禁用
如果用户禁用了 JavaScript,帧破坏代码无法运行。
2. 可以被绕过
攻击者可以使用 sandbox 属性:
<!-- 攻击者的页面 -->
<iframe src="https://target-site.com"sandbox="allow-scripts allow-forms allow-same-origin"onload="this.contentWindow.frameElement = null;">
</iframe>
或者使用 CSS 覆盖:
/* 攻击者可以阻止显示警告 */
iframe {pointer-events: none;
}
3. 不应该单独依赖
帧破坏应该作为补充措施,而不是主要防护手段。
最佳实践
✅ 推荐做法:
- 主要防护:使用
X-Frame-Options或 CSPframe-ancestors(服务器端) - 补充防护:使用 JavaScript 帧破坏(客户端)
- 深度防御:多层防护措施
防护措施四:SameSite Cookie 属性
什么是 SameSite?
SameSite 是 Cookie 的一个属性,用于防止跨站请求伪造(CSRF)攻击,同时也能在一定程度上帮助防护点击劫持。
SameSite 的值
1. SameSite=Strict - 严格模式
// 设置 Cookie
Set-Cookie: sessionId=abc123; SameSite=Strict; Secure; HttpOnly
效果:
- Cookie 只在同站请求中发送
- 完全禁止跨站发送 Cookie
- 即使点击链接跳转也不会发送 Cookie
适用场景:
- 银行网站
- 高安全要求的应用
2. SameSite=Lax - 宽松模式(推荐)
Set-Cookie: sessionId=abc123; SameSite=Lax; Secure; HttpOnly
效果:
- GET 请求允许跨站发送 Cookie(例如从搜索引擎点击链接)
- POST 请求禁止跨站发送 Cookie
- 在 iframe 中加载页面时,不发送 Cookie
适用场景:
- 大多数网站(推荐使用)
- 需要 SEO 友好的网站
3. SameSite=None - 无限制
Set-Cookie: sessionId=abc123; SameSite=None; Secure; HttpOnly
效果:
- 允许跨站发送 Cookie
- ⚠️ 必须配合
Secure使用(HTTPS)
适用场景:
- 需要在第三方网站嵌入的页面
- 单点登录(SSO)场景
SameSite 如何帮助防护点击劫持?
当页面被嵌入到跨站 iframe 中时:
- 如果 Cookie 设置了
SameSite=Lax或Strict - Cookie 不会被发送到服务器
- 即使用户已登录,iframe 中的页面也无法获取登录状态
- 攻击者无法利用用户的登录状态进行恶意操作
代码示例
Node.js/Express
const express = require('express');
const session = require('express-session');
const app = express();app.use(session({secret: 'your-secret-key',resave: false,saveUninitialized: false,cookie: {secure: true, // 只在 HTTPS 下发送httpOnly: true, // 防止 XSS 攻击sameSite: 'lax', // 防护点击劫持和 CSRFmaxAge: 24 * 60 * 60 * 1000 // 24 小时}
}));
PHP
<?php
// 设置 Cookie
setcookie('sessionId','abc123',['secure' => true, // 只在 HTTPS 下发送'httponly' => true, // 防止 XSS'samesite' => 'Lax' // 防护点击劫持]
);
?>
直接设置 HTTP 响应头
app.use((req, res, next) => {// 设置 Cookie(示例)res.cookie('sessionId', 'abc123', {secure: true,httpOnly: true,sameSite: 'lax'});next();
});
SameSite 的浏览器支持
- Chrome 51+
- Firefox 60+
- Safari 12+
- Edge 79+
所有现代浏览器都支持 SameSite 属性。
完整防护方案(推荐)
多层次防护策略
单一防护措施可能被绕过,建议使用**深度防御(Defense in Depth)**策略。
推荐配置(生产环境)
1. 服务器端响应头配置
// Node.js/Express 完整示例
const express = require('express');
const helmet = require('helmet'); // 推荐使用 helmet
const app = express();// 使用 helmet 自动设置安全响应头
app.use(helmet({frameguard: { action: 'sameorigin' }, // X-Frame-Options: SAMEORIGINcontentSecurityPolicy: {directives: {defaultSrc: ["'self'"],styleSrc: ["'self'", "'unsafe-inline'"],scriptSrc: ["'self'"],imgSrc: ["'self'", "data:", "https:"],frameAncestors: ["'self'"], // 防护点击劫持// ... 其他 CSP 指令},},
}));// 或者手动设置
app.use((req, res, next) => {// 主要防护:CSP frame-ancestorsres.setHeader("Content-Security-Policy","frame-ancestors 'self'; default-src 'self';");// 向后兼容:X-Frame-Optionsres.setHeader("X-Frame-Options", "SAMEORIGIN");// Cookie 防护res.cookie('sessionId', req.sessionID, {secure: true,httpOnly: true,sameSite: 'lax'});next();
});app.listen(3000);
2. 前端 JavaScript 帧破坏
在 HTML 的 <head> 中立即执行:
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>受保护的页面</title><!-- 帧破坏脚本(立即执行) --><script>(function() {if (window.top !== window.self) {try {window.top.location = window.self.location;} catch (e) {document.body.innerHTML = '<div style="text-align:center;padding:50px;"><h1>⚠️ 安全警告</h1><p>此页面不能嵌入到其他网页中。</p><a href="' + window.location.href + '" target="_blank">在新窗口中打开</a></div>';}}})();</script>
</head>
<body><!-- 页面内容 -->
</body>
</html>
3. Nginx 完整配置示例
server {listen 443 ssl http2;server_name example.com;# SSL 配置...# 安全响应头add_header X-Frame-Options "SAMEORIGIN" always;add_header Content-Security-Policy "frame-ancestors 'self'; default-src 'self';" always;add_header X-Content-Type-Options "nosniff" always;add_header X-XSS-Protection "1; mode=block" always;# Cookie 设置(在应用层设置)location / {proxy_pass http://backend;# ... 其他代理配置}# 敏感页面更严格的防护location ~ ^/(payment|transfer|admin) {add_header X-Frame-Options "DENY" always;add_header Content-Security-Policy "frame-ancestors 'none';" always;proxy_pass http://backend;}
}
不同场景的防护策略
场景一:公开内容网站(如博客)
// 允许嵌入,但需要验证来源
res.setHeader("Content-Security-Policy", "frame-ancestors 'self' https://trusted-partner.com");
res.setHeader("X-Frame-Options", "SAMEORIGIN");
场景二:银行/支付网站
// 完全禁止嵌入
res.setHeader("Content-Security-Policy", "frame-ancestors 'none'");
res.setHeader("X-Frame-Options", "DENY");// 加上 JavaScript 帧破坏
// 加上 SameSite=Lax Cookie
场景三:需要嵌入的页面(如视频播放器)
// 允许嵌入,但设置 SameSite=None Cookie(需要 HTTPS)
res.setHeader("Content-Security-Policy", "frame-ancestors *");
res.cookie('sessionId', value, { sameSite: 'none', secure: true });
验证防护是否生效
测试脚本
<!-- test-clickjacking.html -->
<!DOCTYPE html>
<html>
<head><title>点击劫持测试</title>
</head>
<body><h1>测试:尝试嵌入目标网站</h1><iframe src="https://your-website.com" width="100%" height="600px"id="test-frame"></iframe><div id="result"></div><script>const iframe = document.getElementById('test-frame');const result = document.getElementById('result');iframe.onload = function() {try {// 尝试访问 iframe 内容const iframeContent = iframe.contentDocument || iframe.contentWindow.document;if (iframeContent && iframeContent.body) {result.innerHTML = '<p style="color: red;">❌ 防护失败:页面可以被嵌入</p>';}} catch (e) {result.innerHTML = '<p style="color: green;">✅ 防护成功:页面被阻止嵌入(' + e.message + ')</p>';}};setTimeout(() => {if (iframe.contentDocument === null && iframe.contentWindow.document === null) {result.innerHTML = '<p style="color: green;">✅ 防护成功:无法访问 iframe 内容</p>';}}, 2000);</script>
</body>
</html>
使用命令行工具
# 检查响应头
curl -I https://your-website.com | grep -E "(X-Frame-Options|Content-Security-Policy)"# 预期输出:
# X-Frame-Options: SAMEORIGIN
# Content-Security-Policy: frame-ancestors 'self';
前端开发者的防护责任
开发时需要注意的事项
1. 不要轻易使用 iframe
// ❌ 不好的做法:直接嵌入第三方页面
<iframe src="https://bank.com/login"></iframe>// ✅ 好的做法:在新窗口打开
<a href="https://bank.com/login" target="_blank">登录</a>
2. 如果必须使用 iframe,验证来源
// 只嵌入可信任的来源
const trustedDomains = ['https://trusted-partner.com'];function isValidSource(url) {return trustedDomains.some(domain => url.startsWith(domain));
}if (isValidSource(iframeSrc)) {// 允许嵌入
} else {// 拒绝嵌入
}
3. 检查自己网站的响应头
// 在开发环境定期检查
fetch('/api/check-headers').then(res => {const frameOptions = res.headers.get('X-Frame-Options');const csp = res.headers.get('Content-Security-Policy');if (!frameOptions && !csp?.includes('frame-ancestors')) {console.warn('⚠️ 警告:未检测到点击劫持防护!');}});
4. 敏感操作页面加强防护
// 在支付页面、设置页面等敏感页面
if (window.location.pathname.includes('/payment')) {// 双重检查:服务器端 + 客户端if (window.top !== window.self) {window.top.location = window.self.location;}
}
使用安全库
推荐:helmet.js(Node.js)
npm install helmet
const helmet = require('helmet');
app.use(helmet({frameguard: { action: 'sameorigin' },contentSecurityPolicy: {directives: {frameAncestors: ["'self'"],},},
}));
常见问题与最佳实践
常见问题
Q1: X-Frame-Options 和 CSP frame-ancestors 哪个更好?
A: CSP frame-ancestors 是更现代的标准,功能更强大(可以指定多个域名)。但建议同时使用两者:
- CSP
frame-ancestors:现代浏览器 X-Frame-Options:向后兼容旧浏览器
Q2: 如果我的网站需要被嵌入到其他网站怎么办?
A: 使用 CSP 指定允许的域名:
res.setHeader("Content-Security-Policy","frame-ancestors 'self' https://trusted-partner1.com https://trusted-partner2.com"
);
⚠️ 注意: 只允许你完全信任的域名。
Q3: JavaScript 帧破坏够用吗?
A: 不够。JavaScript 可以被禁用或绕过。帧破坏只能作为补充措施,主要防护应该依赖服务器端的响应头。
Q4: SameSite Cookie 会影响用户体验吗?
A: SameSite=Lax 对用户体验影响很小:
- 正常的页面跳转不受影响
- 从搜索引擎点击链接可以正常登录
- 只有在 iframe 中加载时才不发送 Cookie(这正是我们想要的安全行为)
Q5: 如何测试我的防护是否生效?
A:
- 创建一个测试页面,尝试用 iframe 嵌入你的网站
- 检查响应头是否包含
X-Frame-Options或 CSPframe-ancestors - 使用在线工具:https://securityheaders.com/
Q6: 点击劫持只影响网站吗?
A: 主要影响 Web 应用,但移动应用(WebView)也可能受到影响。
最佳实践总结
✅ 推荐做法
-
多层防护
- 服务器端:
X-Frame-Options+ CSPframe-ancestors - 客户端:JavaScript 帧破坏(补充)
- Cookie:
SameSite=Lax
- 服务器端:
-
敏感页面加强防护
- 支付页面:
DENY或'none' - 普通页面:
SAMEORIGIN或'self'
- 支付页面:
-
定期检查
- 使用工具检查响应头
- 测试是否可以嵌入 iframe
-
保持更新
- 关注安全公告
- 及时更新防护措施
❌ 避免的做法
-
不要单独依赖 JavaScript 防护
- JavaScript 可以被禁用
-
不要完全禁止嵌入(如果确实需要)
- 使用 CSP 指定允许的域名
-
不要忽略旧浏览器
- 同时设置
X-Frame-Options和 CSP
- 同时设置
-
不要在生产环境关闭防护进行测试
- 即使短暂关闭也可能被利用
总结
点击劫持是一种危险的攻击方式,但通过正确的防护措施可以有效防范:
核心防护措施
- X-Frame-Options 响应头:基础防护,向后兼容
- CSP frame-ancestors:现代标准,功能强大
- JavaScript 帧破坏:补充防护,深度防御
- SameSite Cookie:防止利用登录状态
关键要点
- ✅ 多层防护:不要依赖单一措施
- ✅ 服务器端优先:主要防护在服务器端
- ✅ 敏感页面加强:支付等页面使用最严格的防护
- ✅ 定期检查:使用工具验证防护是否生效
行动起来
立即检查你的网站:
- 检查响应头是否有防护措施
- 测试是否可以嵌入 iframe
- 根据需求选择适合的防护策略
- 实施并验证防护效果
保护用户就是保护你的业务!🛡️
参考资料
- OWASP Clickjacking Defense Cheat Sheet
- MDN: X-Frame-Options
- MDN: Content-Security-Policy
- MDN: SameSite Cookies
