CSRF 攻击与防护
CSRF 概述
CSRF (Cross-Site Request Forgery) 跨站请求伪造,攻击者诱导用户访问恶意网站,在用户不知情的情况下执行操作。
CSRF 攻击原理
攻击流程
1. 用户登录正常网站 A
2. 网站 A 设置 Cookie
3. 用户访问恶意网站 B
4. 网站 B 发起对网站 A 的请求
5. 浏览器自动携带 Cookie
6. 网站 A 认为请求来自用户
7. 执行恶意操作
攻击示例
场景:转账操作
<!-- 正常网站转账表单 -->
<form action="https://bank.com/transfer" method="POST">
<input name="to" value="account123">
<input name="amount" value="1000">
</form>
<!-- 恶意网站 -->
<img src="https://bank.com/transfer?to=attacker&amount=10000">
用户访问恶意网站时,浏览器会自动发送请求,携带 Cookie,完成转账。
CSRF 攻击类型
1. GET 请求攻击
特点:
- 利用图片、链接等
- 简单易实现
- 不需要用户交互
示例:
<!-- 恶意网站 -->
<img src="https://bank.com/transfer?to=attacker&amount=10000">
2. POST 请求攻击
特点:
- 需要表单提交
- 可以隐藏表单
- 自动提交
示例:
<!-- 恶意网站 -->
<form id="attack" action="https://bank.com/transfer" method="POST">
<input name="to" value="attacker">
<input name="amount" value="10000">
</form>
<script>document.getElementById('attack').submit();</script>
3. JSON 请求攻击
特点:
- 现代应用常用
- 需要特殊处理
- 利用 CORS
示例:
// 恶意网站
fetch('https://api.example.com/user/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email: 'attacker@evil.com' }),
credentials: 'include', // 携带 Cookie
});
CSRF 防护措施
1. CSRF Token
原理:
- 服务器生成随机 Token
- 存储在 Session 或 Cookie
- 请求时验证 Token
实现:
// 服务端生成 Token
const token = crypto.randomBytes(32).toString('hex');
session.csrfToken = token;
// 返回给客户端
res.render('form', { csrfToken: token });
// 客户端提交
<form method="POST">
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
<!-- 其他字段 -->
</form>
// 服务端验证
if (req.body._csrf !== session.csrfToken) {
return res.status(403).send('Invalid CSRF token');
}
双重 Cookie:
// 设置 Cookie
res.cookie('csrf-token', token, { httpOnly: false });
// 客户端读取并发送
const token = document.cookie.match(/csrf-token=([^;]+)/)[1];
fetch('/api/update', {
headers: {
'X-CSRF-Token': token,
},
});
2. SameSite Cookie
设置:
Set-Cookie: sessionId=abc123; SameSite=Strict; Secure
值:
Strict:完全禁止跨站携带Lax:GET 请求可以携带None:允许跨站携带(需要 Secure)
效果:
- 防止跨站请求携带 Cookie
- 浏览器原生支持
3. 验证 Referer/Origin
检查请求头:
// 验证 Referer
const referer = req.headers.referer;
if (!referer || !referer.startsWith('https://example.com')) {
return res.status(403).send('Invalid referer');
}
// 验证 Origin
const origin = req.headers.origin;
const allowedOrigins = ['https://example.com'];
if (!allowedOrigins.includes(origin)) {
return res.status(403).send('Invalid origin');
}
注意:
- Referer 可能被过滤
- Origin 更可靠
- 需要白名单
4. 自定义请求头
设置:
// 客户端
fetch('/api/update', {
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-Custom-Header': 'value',
},
});
// 服务端验证
if (!req.headers['x-requested-with']) {
return res.status(403).send('Missing custom header');
}
原理:
- 浏览器同源策略限制
- 跨站请求无法设置自定义头
- 简单有效
5. 双重提交 Cookie
实现:
// 服务端设置 Cookie
res.cookie('csrf-token', token, { httpOnly: false });
// 客户端读取
const token = getCookie('csrf-token');
// 请求时发送
fetch('/api/update', {
headers: {
'X-CSRF-Token': token,
},
credentials: 'include',
});
// 服务端验证
const cookieToken = req.cookies['csrf-token'];
const headerToken = req.headers['x-csrf-token'];
if (cookieToken !== headerToken) {
return res.status(403).send('Invalid CSRF token');
}
框架防护
Express.js
使用 csrf 中间件:
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
app.use(csrfProtection);
app.get('/form', csrfProtection, (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});
app.post('/process', csrfProtection, (req, res) => {
// 已验证
});
Django
内置 CSRF 保护:
# settings.py
MIDDLEWARE = [
'django.middleware.csrf.CsrfViewMiddleware',
]
# 模板
<form method="post">
{% csrf_token %}
<!-- 表单字段 -->
</form>
Spring Security
配置:
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
return http.build();
}
}
最佳实践
1. 多层防护
- CSRF Token
- SameSite Cookie
- 验证 Referer/Origin
2. 敏感操作额外验证
- 密码确认
- 二次验证
- 操作日志
3. 使用框架防护
- Express csrf
- Django CSRF
- Spring Security
4. 定期安全审计
- 检查防护措施
- 测试攻击场景
- 更新依赖
常见问题
1. Token 存储在哪里?
选项:
- Session(服务端)
- Cookie(客户端,HttpOnly)
- 双重 Cookie(客户端,非 HttpOnly)
2. Token 如何更新?
策略:
- 每次请求后更新
- 定期更新
- 固定 Token(不推荐)
3. AJAX 请求如何处理?
方法:
- 从 Cookie 读取 Token
- 从 Meta 标签读取
- 自定义请求头