【前端面试小册】网络-第14节:点击劫持与iframe安全深度解析,前端安全防御进阶指南(CSP实战
🎯 本章核心:掌握点击劫持攻击原理、iframe安全防护、CSP高级配置,构建企业级前端安全防线
一、现实世界类比 🎭
点击劫持是什么?
想象你去ATM机取款:
- 正常情况:你看到的屏幕就是银行系统,点击"取款"就是取款
- 点击劫持:有人在ATM屏幕上覆盖了一层透明薄膜
- 薄膜上写着"点击领取优惠券"
- 你以为在领优惠券,实际点击的是"转账10000元"
- 你的钱就这样被偷走了
换到网页上:
┌──────────────────────────────────────────────┐
│ 恶意网站(用户看到的) │
│ ┌────────────────────────────────────────┐ │
│ │ 🎁 恭喜中奖! │ │
│ │ │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ 点击这里领取 iPhone 15 │ │ │
│ │ └─────────────────────────────┘ │ │
│ └────────────────────────────────────────┘ │
│ ↑ │
│ 透明 iframe(用户看不到) │
│ ┌────────────────────────────────────────┐ │
│ │ 银行转账页面: │ │
│ │ 转账金额:10000元 │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ 确认转账 │←实际按钮│
│ │ └─────────────────────────────┘ │ │
│ └────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
💡 关键理解:点击劫持的本质是视觉欺骗 + iframe嵌套
二、点击劫持攻击原理 🔍
2.1 技术原理图解
攻击流程:
用户 恶意网站 银行网站
│ │ │
│ 1.访问恶意网站 │ │
│ ─────────────────► │ │
│ │ │
│ │ 2.iframe嵌入银行页面 │
│ │ ─────────────────────►│
│ │ │
│ │ 3.设置透明+定位覆盖 │
│ │ ◄─────────────────────│
│ │ │
│ 4.点击"领奖按钮" │ │
│ ─────────────────► │ │
│ │ │
│ │ 5.实际触发转账按钮 │
│ │ ─────────────────────►│
│ │ │
│ │ 6.转账成功! │
│ ◄───────────────────────────────────────── │
2.2 攻击代码示例
<!-- 恶意网站 evil.com 的页面 -->
<!DOCTYPE html>
<html>
<head>
<title>🎁 恭喜中奖!点击领取大奖</title>
<style>
/* 诱惑用户的按钮 */
.fake-button {
position: absolute;
top: 300px;
left: 200px;
width: 200px;
height: 50px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 25px;
font-size: 18px;
cursor: pointer;
z-index: 1; /* 在下层,用户看得到 */
}
/* 透明的银行页面 iframe */
.hidden-frame {
position: absolute;
top: 280px; /* 精确定位,让银行的"确认转账"按钮 */
left: 180px; /* 正好覆盖在"领奖按钮"上方 */
width: 400px;
height: 200px;
opacity: 0; /* ⚠️ 关键:完全透明,用户看不到 */
z-index: 2; /* 在上层,实际接收点击 */
}
</style>
</head>
<body>
<h1>🎉 恭喜您获得 iPhone 15 Pro!</h1>
<p>点击下方按钮立即领取:</p>
<!-- 用户看到的假按钮 -->
<button class="fake-button">📱 点击领取 iPhone</button>
<!-- 用户看不到的真实银行页面 -->
<iframe
class="hidden-frame"
src="https://bank.com/transfer?to=hacker&amount=10000"
></iframe>
</body>
</html>
⚠️ 危害分析:用户以为点击领奖,实际触发了银行转账!浏览器会自动携带银行的 Cookie,转账请求完全合法。
三、前端真实业务场景
场景1:社交媒体关注劫持
<!-- 攻击者网站 -->
<style>
.twitter-frame {
opacity: 0;
position: absolute;
top: 100px;
left: 50px;
}
</style>
<!-- 用户看到的 -->
<button>免费下载资源</button>
<!-- 用户看不到的 Twitter 关注按钮 -->
<iframe class="twitter-frame" src="https://twitter.com/intent/follow?user_id=attacker123">
</iframe>
<!--
结果:用户以为下载资源,实际关注了攻击者的 Twitter 账号
这种攻击叫 "Likejacking"(点赞劫持)或 "Followjacking"(关注劫持)
-->
场景2:后台管理系统权限提升
<!--
攻击场景:
1. 管理员已登录公司后台 admin.company.com
2. 攻击者发送"年会抽奖"链接给管理员
3. 管理员点击链接,进入恶意网站
-->
<style>
.admin-frame {
opacity: 0.0001; /* 几乎完全透明 */
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 999;
}
</style>
<h1>🎊 公司年会抽奖系统</h1>
<button>点击抽奖</button>
<!-- 嵌入后台的"添加管理员"页面 -->
<iframe
class="admin-frame"
src="https://admin.company.com/users/add?role=admin&**********"
></iframe>
<!-- 结果:管理员以为在抽奖,实际添加了攻击者为管理员 -->
场景3:电商下单劫持
// 攻击场景:用户想买 100 元的商品,实际买了 10000 元
// 恶意网站代码
const evilPage = `
<style>
.shop-frame {
opacity: 0;
position: absolute;
top: 200px;
left: 100px;
width: 500px;
height: 300px;
}
.buy-button {
position: absolute;
top: 350px;
left: 150px;
/* 定位到 iframe 中"立即购买"按钮的位置 */
}
</style>
<h1>限时特价!iPhone 只要 100 元!</h1>
<button class="buy-button">立即抢购</button>
<!-- 实际是购买 10000 元商品的页面 -->
<iframe
class="shop-frame"
src="https://shop.com/product/expensive-item?price=10000&action=buy"
></iframe>
`;
四、防御方案完整解析 🛡️
4.1 X-Frame-Options 响应头(传统方案)
// ===== Node.js/Express 配置 =====
const express = require('express');
const app = express();
// 设置 X-Frame-Options 响应头
app.use((req, res, next) => {
// 三种选项:
// 1️⃣ DENY:完全禁止被 iframe 嵌入
res.setHeader('X-Frame-Options', 'DENY');
// 2️⃣ SAMEORIGIN:只允许同源网站嵌入
// res.setHeader('X-Frame-Options', 'SAMEORIGIN');
// 3️⃣ ALLOW-FROM:只允许指定来源嵌入(已废弃,不推荐)
// res.setHeader('X-Frame-Options', 'ALLOW-FROM https://trusted.com');
next();
});
// ===== Nginx 配置 =====
/*
server {
# 在 server 或 http 块中设置
add_header X-Frame-Options "SAMEORIGIN" always;
# 或者完全禁止
add_header X-Frame-Options "DENY" always;
}
*/
// ===== Apache 配置 =====
/*
# 在 .htaccess 或 httpd.conf 中
Header always set X-Frame-Options "SAMEORIGIN"
*/
三种值的对比:
| 值 | 含义 | 使用场景 |
|---|---|---|
DENY |
禁止任何网站嵌入 | 银行、支付等高安全页面 |
SAMEORIGIN |
只允许同源嵌入 | 大部分业务系统 |
ALLOW-FROM uri |
只允许指定来源 | ⚠️ 已废弃,兼容性差 |
4.2 CSP frame-ancestors(现代方案)⭐⭐⭐⭐⭐
// ===== CSP 是 X-Frame-Options 的升级版 =====
// Node.js/Express 配置
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
// 禁止被任何网站嵌入
"frame-ancestors 'none';"
// 或:只允许同源嵌入
// "frame-ancestors 'self';"
// 或:只允许特定域名嵌入
// "frame-ancestors 'self' https://partner.com https://trusted.com;"
);
next();
});
// ===== 完整的 CSP 安全配置 =====
const securityHeaders = {
'Content-Security-Policy': [
"default-src 'self'", // 默认只允许同源
"script-src 'self'", // 脚本只允许同源
"style-src 'self' 'unsafe-inline'", // 样式允许内联
"img-src 'self' https: data:", // 图片允许 HTTPS 和 data:
"font-src 'self'", // 字体只允许同源
"connect-src 'self' https://api.example.com", // API 请求
"frame-ancestors 'none'", // ⭐ 防止点击劫持
"base-uri 'self'", // 防止 base 标签劫持
"form-action 'self'" // 表单只能提交到同源
].join('; ')
};
app.use((req, res, next) => {
Object.entries(securityHeaders).forEach(([key, value]) => {
res.setHeader(key, value);
});
next();
});
// ===== Nginx 配置 =====
/*
server {
add_header Content-Security-Policy "frame-ancestors 'self';" always;
}
*/
CSP vs X-Frame-Options 对比:
| 特性 | X-Frame-Options | CSP frame-ancestors |
|---|---|---|
| 灵活性 | 低(只有3个选项) | 高(可配置多个域名) |
| 浏览器支持 | 老旧浏览器也支持 | 现代浏览器支持 |
| 优先级 | 被 CSP 覆盖 | 更高优先级 |
| 推荐程度 | ⚠️ 过渡方案 | ✅ 推荐使用 |
💡 最佳实践:同时设置两个头,兼顾兼容性和安全性
4.3 前端 JavaScript 防御(双保险)
// ===== 检测页面是否被嵌入 iframe =====
// 方法1:检测 window.top
function checkFrameEmbedding() {
try {
// 如果被嵌入,window.top !== window.self
if (window.top !== window.self) {
console.warn('⚠️ 检测到页面被嵌入 iframe!');
// 处理方式1:跳转到顶层
window.top.location = window.self.location;
// 处理方式2:清空页面
// document.body.innerHTML = '请直接访问本站';
// 处理方式3:显示警告
// alert('安全警告:请直接访问本站!');
}
} catch (e) {
// 如果跨域,访问 window.top 会报错
console.error('⚠️ 页面被跨域嵌入!');
document.body.innerHTML = '<h1>请直接访问本站</h1>';
}
}
// 页面加载时检测
checkFrameEmbedding();
// 方法2:使用 frameElement 检测
function checkFrameElement() {
if (window.frameElement) {
// frameElement 存在,说明当前页面在 iframe 中
console.warn('⚠️ 当前页面在 iframe 中');
return true;
}
return false;
}
// 方法3:Busting Frame(破坏框架)
// 在 <head> 中尽早执行
(function() {
if (self !== top) {
// 方案A:强制跳出 iframe
top.location = self.location;
// 方案B:隐藏内容(防止在跳转前被利用)
document.documentElement.style.display = 'none';
}
})();
Vue 组件实现:
<!-- SecurityGuard.vue -->
<template>
<div v-if="isSafe">
<slot></slot>
</div>
<div v-else class="security-warning">
<h1>⚠️ 安全警告</h1>
<p>检测到页面被非法嵌入,请直接访问本站</p>
<button @click="redirectToTop">前往安全页面</button>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const isSafe = ref(true)
onMounted(() => {
// 检测是否被嵌入
try {
if (window.top !== window.self) {
isSafe.value = false
console.warn('页面被嵌入 iframe,可能存在点击劫持风险')
}
} catch (e) {
// 跨域嵌入
isSafe.value = false
}
})
function redirectToTop() {
try {
window.top.location = window.self.location
} catch (e) {
window.open(window.self.location.href, '_blank')
}
}
</script>
<style scoped>
.security-warning {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
background: #fee2e2;
color: #dc2626;
}
.security-warning button {
margin-top: 20px;
padding: 12px 24px;
background: #dc2626;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
}
</style>
React Hook 实现:
// useFrameProtection.js
import { useState, useEffect } from 'react';
export function useFrameProtection() {
const [isFramed, setIsFramed] = useState(false);
const [isCrossOrigin, setIsCrossOrigin] = useState(false);
useEffect(() => {
try {
if (window.top !== window.self) {
setIsFramed(true);
console.warn('检测到页面被嵌入 iframe');
}
} catch (e) {
// 跨域访问 window.top 会抛出异常
setIsFramed(true);
setIsCrossOrigin(true);
console.error('页面被跨域嵌入');
}
}, []);
const escapeFrame = () => {
try {
window.top.location = window.self.location;
} catch (e) {
window.open(window.self.location.href, '_blank');
}
};
return {
isFramed,
isCrossOrigin,
escapeFrame,
isSafe: !isFramed
};
}
// SecurityWrapper.jsx
import { useFrameProtection } from './useFrameProtection';
export function SecurityWrapper({ children }) {
const { isSafe, escapeFrame } = useFrameProtection();
if (!isSafe) {
return (
<div className="security-warning">
<h1>⚠️ 安全警告</h1>
<p>检测到页面被非法嵌入</p>
<button onClick={escapeFrame}>前往安全页面</button>
</div>
);
}
return <>{children}</>;
}
// App.jsx 使用
import { SecurityWrapper } from './SecurityWrapper';
function App() {
return (
<SecurityWrapper>
<div>
<h1>银行转账系统</h1>
{/* 你的应用内容 */}
</div>
</SecurityWrapper>
);
}
五、企业级安全配置实战 🏢
5.1 完整的 Express 安全中间件
// security.middleware.js
const helmet = require('helmet');
// 创建安全中间件
function createSecurityMiddleware(options = {}) {
const {
allowedFrameAncestors = ["'none'"], // 默认禁止被嵌入
reportUri = '/csp-report', // CSP 违规报告地址
isDevelopment = process.env.NODE_ENV === 'development'
} = options;
return [
// 1️⃣ 使用 helmet 设置基础安全头
helmet({
// 点击劫持防护
frameguard: {
action: 'deny' // 或 'sameorigin'
},
// XSS 防护
xssFilter: true,
// 内容类型嗅探防护
noSniff: true,
// HTTPS 强制
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}),
// 2️⃣ 自定义 CSP 配置
(req, res, next) => {
const cspDirectives = [
"default-src 'self'",
`script-src 'self' ${isDevelopment ? "'unsafe-eval'" : ''}`,
"style-src 'self' 'unsafe-inline'",
"img-src 'self' https: data: blob:",
"font-src 'self' https://fonts.gstatic.com",
"connect-src 'self' https://api.example.com",
`frame-ancestors ${allowedFrameAncestors.join(' ')}`,
"base-uri 'self'",
"form-action 'self'",
`report-uri ${reportUri}`
];
res.setHeader(
'Content-Security-Policy',
cspDirectives.join('; ')
);
next();
},
// 3️⃣ CSP 违规报告处理
(req, res, next) => {
if (req.path === reportUri && req.method === 'POST') {
console.log('CSP 违规报告:', req.body);
// 可以发送到日志系统或告警系统
return res.status(204).end();
}
next();
}
];
}
module.exports = { createSecurityMiddleware };
// app.js 使用
const express = require('express');
const { createSecurityMiddleware } = require('./security.middleware');
const app = express();
// 应用安全中间件
app.use(createSecurityMiddleware({
allowedFrameAncestors: ["'self'", 'https://partner.com'],
isDevelopment: process.env.NODE_ENV === 'development'
}));
// 你的路由...
app.get('/', (req, res) => {
res.send('Hello, Secure World!');
});
5.2 Nginx 完整安全配置
# /etc/nginx/conf.d/security.conf
server {
listen 443 ssl http2;
server_name example.com;
# SSL 配置
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
# ========== 安全响应头 ==========
# 1️⃣ 防止点击劫持
add_header X-Frame-Options "SAMEORIGIN" always;
# 2️⃣ CSP 配置(更灵活的点击劫持防护)
add_header Content-Security-Policy "
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' https: data:;
font-src 'self';
connect-src 'self' https://api.example.com;
frame-ancestors 'self';
base-uri 'self';
form-action 'self';
" always;
# 3️⃣ XSS 防护
add_header X-XSS-Protection "1; mode=block" always;
# 4️⃣ 防止 MIME 类型嗅探
add_header X-Content-Type-Options "nosniff" always;
# 5️⃣ 强制 HTTPS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# 6️⃣ 控制 Referer 信息
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# 7️⃣ 权限策略(禁用危险 API)
add_header Permissions-Policy "
geolocation=(),
microphone=(),
camera=(),
payment=()
" always;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
5.3 Vue/React 项目的 CSP 配置
Vue 项目(vue.config.js):
// vue.config.js
module.exports = {
devServer: {
headers: {
'X-Frame-Options': 'SAMEORIGIN',
'Content-Security-Policy': [
"default-src 'self'",
"script-src 'self' 'unsafe-eval'", // 开发环境需要 eval
"style-src 'self' 'unsafe-inline'",
"frame-ancestors 'self'"
].join('; ')
}
},
// 生产环境通过 Nginx/后端设置更严格的 CSP
};
React 项目(自定义服务器):
// server.js
const express = require('express');
const path = require('path');
const app = express();
// 安全头配置
app.use((req, res, next) => {
// 生产环境 CSP(更严格)
const csp = process.env.NODE_ENV === 'production'
? "default-src 'self'; script-src 'self'; frame-ancestors 'none';"
: "default-src 'self'; script-src 'self' 'unsafe-eval'; frame-ancestors 'self';";
res.setHeader('Content-Security-Policy', csp);
res.setHeader('X-Frame-Options', 'DENY');
next();
});
// 静态文件服务
app.use(express.static(path.join(__dirname, 'build')));
// SPA 路由
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'build', 'index.html'));
});
app.listen(3000);
六、图解总结 📊
点击劫持攻击与防御全景图
┌─────────────────────────────────────────────────────────────────────┐
│ 点击劫持攻击与防御 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 【攻击原理】 │
│ │
│ 恶意网站 用户浏览器 │
│ ┌─────────┐ ┌─────────────┐ │
│ │ 诱惑按钮 │ + 透明 iframe → │ 点击后触发 │ │
│ │ (可见) │ (不可见) │ 真实操作 │ │
│ └─────────┘ └─────────────┘ │
│ │
│ 【三层防御体系】 │
│ │
│ 第一层:HTTP 响应头 │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ X-Frame-Options: DENY │ │
│ │ Content-Security-Policy: frame-ancestors 'none'; │ │
│ └───────────────────────────────────────────────────────────┘ │
│ ↓ │
│ 第二层:前端 JavaScript 检测 │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ if (window.top !== window.self) { │ │
│ │ // 跳出 iframe 或显示警告 │ │
│ │ } │ │
│ └───────────────────────────────────────────────────────────┘ │
│ ↓ │
│ 第三层:关键操作二次确认 │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 转账、删除等敏感操作: │ │
│ │ - 输入密码验证 │ │
│ │ - 短信验证码 │ │
│ │ - 人机验证 │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
安全响应头速查表
┌─────────────────────┬────────────────────────────────────────────────┐
│ 响应头 │ 作用 │
├─────────────────────┼────────────────────────────────────────────────┤
│ X-Frame-Options │ 防止点击劫持(传统方案) │
│ │ 值: DENY | SAMEORIGIN | ALLOW-FROM │
├─────────────────────┼────────────────────────────────────────────────┤
│ Content-Security- │ 防止点击劫持(现代方案)+ XSS 防护 │
│ Policy │ 核心指令: frame-ancestors, script-src │
├─────────────────────┼────────────────────────────────────────────────┤
│ X-Content-Type- │ 防止 MIME 类型嗅探攻击 │
│ Options: nosniff │ │
├─────────────────────┼────────────────────────────────────────────────┤
│ Strict-Transport- │ 强制 HTTPS(HSTS) │
│ Security │ │
├─────────────────────┼────────────────────────────────────────────────┤
│ Referrer-Policy │ 控制 Referer 头信息泄露 │
├─────────────────────┼────────────────────────────────────────────────┤
│ Permissions-Policy │ 禁用危险浏览器 API │
└─────────────────────┴────────────────────────────────────────────────┘
七、面试高频考点 🎯
考点1:什么是点击劫持?如何防御?
标准答案模板:
## 点击劫持定义
点击劫持(Clickjacking)是一种视觉欺骗攻击。
攻击者通过透明 iframe 覆盖在诱惑性内容上,
诱导用户点击看似无害的按钮,实际触发恶意操作。
## 防御方案(按优先级)
1. **CSP frame-ancestors**(推荐)
Content-Security-Policy: frame-ancestors 'self';
2. **X-Frame-Options 响应头**
X-Frame-Options: DENY;
3. **前端 JavaScript 检测**
if (window.top !== window.self) {
window.top.location = window.self.location;
}
4. **敏感操作二次验证**
密码确认、短信验证码等
考点2:X-Frame-Options 和 CSP frame-ancestors 有什么区别?
const comparison = {
'X-Frame-Options': {
'优点': '兼容性好,老浏览器支持',
'缺点': '只能设置一个值,不够灵活',
'值选项': ['DENY', 'SAMEORIGIN', 'ALLOW-FROM(已废弃)'],
'推荐': '作为降级方案'
},
'CSP frame-ancestors': {
'优点': '灵活,可配置多个允许的域名',
'缺点': 'IE11 及以下不支持',
'值选项': ["'none'", "'self'", '任意域名列表'],
'推荐': '主要防护方案'
},
'最佳实践': '两个都设置,CSP 优先级更高'
};
考点3:如何检测页面被 iframe 嵌入?
// 面试答案
function detectIframeEmbedding() {
// 方法1:比较 window 对象
const isFramed = window.top !== window.self;
// 方法2:检查 frameElement
const hasFrameElement = window.frameElement !== null;
// 方法3:跨域检测(访问 top 会报错)
let isCrossOriginFramed = false;
try {
// 尝试访问 top.location
const topLocation = window.top.location.href;
} catch (e) {
isCrossOriginFramed = true;
}
return {
isFramed,
hasFrameElement,
isCrossOriginFramed
};
}
八、关键要点总结 📝
🔑 核心记忆点:
点击劫持本质:透明 iframe + 视觉欺骗
防御三板斧:
- 服务端:
X-Frame-Options+CSP frame-ancestors- 前端:
window.top !== window.self检测- 业务:敏感操作二次确认
配置优先级:CSP > X-Frame-Options > JS 检测
推荐配置:
X-Frame-Options: DENY Content-Security-Policy: frame-ancestors 'none';
💰 本章对应实际工作提升点
前端面试提升点
- ✅ 能清晰讲解点击劫持的攻击原理
- ✅ 知道
X-Frame-Options和CSP frame-ancestors的区别 - ✅ 能手写前端 iframe 检测代码
- ✅ 了解安全响应头的完整配置
业务代码提升点
- ✅ 所有项目默认配置防点击劫持响应头
- ✅ 敏感页面增加 JavaScript 二次检测
- ✅ 核心操作(转账、删除)添加二次确认
- ✅ 建立 CSP 违规监控和告警
架构能力增强点
- ✅ 设计统一的安全中间件
- ✅ 配置 Nginx/CDN 安全响应头
- ✅ 建立安全审计和监控体系
- ✅ 制定团队安全开发规范
模式在团队协作中的价值
- ✅ 统一的安全配置模板,降低配置出错风险
- ✅ 安全中间件复用,减少重复代码
- ✅ 清晰的安全文档,便于新人上手
- ✅ 安全检测工具化,融入 CI/CD 流程
记住:安全防护要层层设防,单一防线永远不够! 🛡️
#前端面试小册##银行##百度##字节##阿里#
查看7道真题和解析