CSRF攻击与防御策略
约 1622 字大约 5 分钟
csrfsecurity
2025-08-15
概述
跨站请求伪造(Cross-Site Request Forgery, CSRF)是一种迫使已登录用户在不知情的情况下执行非预期操作的攻击。攻击者利用浏览器自动发送 Cookie 的行为,伪造用户的合法请求。
攻击原理
攻击载体示例
<!-- 方式 1: 自动提交的隐藏表单 -->
<form action="https://bank.example.com/transfer" method="POST" id="csrf-form">
<input type="hidden" name="to" value="attacker" />
<input type="hidden" name="amount" value="10000" />
</form>
<script>document.getElementById('csrf-form').submit();</script>
<!-- 方式 2: 图片标签(GET 请求 CSRF) -->
<img src="https://bank.example.com/transfer?to=attacker&amount=10000" />
<!-- 方式 3: AJAX(需要 CORS 配合,通常被同源策略阻止读取响应) -->
<script>
fetch('https://bank.example.com/transfer', {
method: 'POST',
credentials: 'include',
body: 'to=attacker&amount=10000',
headers: {'Content-Type': 'application/x-www-form-urlencoded'}
});
</script>防御策略
1. 同步器令牌模式(Synchronizer Token Pattern)
最经典的 CSRF 防御方式:服务端生成一个随机令牌,嵌入表单中,提交时验证。
# Flask 示例
import secrets
from flask import Flask, session, request, abort
@app.before_request
def csrf_protect():
if request.method in ('POST', 'PUT', 'DELETE', 'PATCH'):
token = session.get('csrf_token')
submitted = request.form.get('csrf_token') or request.headers.get('X-CSRF-Token')
if not token or not secrets.compare_digest(token, submitted or ''):
abort(403, "CSRF token validation failed")
@app.before_request
def generate_csrf_token():
if 'csrf_token' not in session:
session['csrf_token'] = secrets.token_hex(32)
# 模板中使用
# <form method="POST">
# <input type="hidden" name="csrf_token" value="{{ session.csrf_token }}">
# ...
# </form>2. 双重提交 Cookie(Double Submit Cookie)
无状态方案:Token 同时存在 Cookie 和请求参数中,服务端比较两者是否一致。
import secrets
from flask import Flask, request, make_response, abort
@app.route('/form', methods=['GET'])
def show_form():
token = secrets.token_hex(32)
response = make_response(render_template('form.html', csrf_token=token))
response.set_cookie(
'csrf_token', token,
httponly=False, # 前端需要读取
secure=True,
samesite='Lax',
max_age=3600
)
return response
@app.before_request
def validate_csrf():
if request.method in ('POST', 'PUT', 'DELETE'):
cookie_token = request.cookies.get('csrf_token')
header_token = request.headers.get('X-CSRF-Token')
if not cookie_token or not header_token:
abort(403)
if not secrets.compare_digest(cookie_token, header_token):
abort(403)// 前端:从 Cookie 读取 Token 并放入请求头
function getCsrfToken() {
return document.cookie
.split('; ')
.find(row => row.startsWith('csrf_token='))
?.split('=')[1];
}
// Axios 全局配置
axios.defaults.headers.common['X-CSRF-Token'] = getCsrfToken();
// Fetch 请求
fetch('/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken(),
},
body: JSON.stringify(data),
});3. SameSite Cookie 属性
SameSite 是现代浏览器原生的 CSRF 防御机制。
# SameSite 属性设置
response.set_cookie(
'session_id', value='abc123',
samesite='Lax', # 推荐:大部分跨站请求不发送 Cookie
secure=True,
httponly=True,
)| 请求类型 | Strict | Lax | None |
|---|---|---|---|
| 顶级导航 GET | 不发送 | 发送 | 发送 |
| 顶级导航 POST | 不发送 | 不发送 | 发送 |
| iframe | 不发送 | 不发送 | 发送 |
| AJAX POST | 不发送 | 不发送 | 发送 |
| 图片请求 | 不发送 | 不发送 | 发送 |
4. Origin/Referer 头检查
from urllib.parse import urlparse
ALLOWED_ORIGINS = {
'https://app.example.com',
'https://www.example.com',
}
@app.before_request
def check_origin():
if request.method in ('POST', 'PUT', 'DELETE'):
origin = request.headers.get('Origin')
referer = request.headers.get('Referer')
# 优先检查 Origin(更可靠,不包含路径)
if origin:
if origin not in ALLOWED_ORIGINS:
abort(403, "Origin not allowed")
return
# 回退到 Referer
if referer:
parsed = urlparse(referer)
check_origin = f"{parsed.scheme}://{parsed.netloc}"
if check_origin not in ALLOWED_ORIGINS:
abort(403, "Referer not allowed")
return
# 两者都不存在时的策略(取决于风险容忍度)
abort(403, "Missing Origin/Referer header")5. 自定义请求头
浏览器的同源策略保证只有同源的 JavaScript 才能添加自定义请求头。利用这一特性可以简单地防御 CSRF。
# 服务端检查自定义头
@app.before_request
def check_custom_header():
if request.method in ('POST', 'PUT', 'DELETE'):
if not request.headers.get('X-Requested-With'):
abort(403, "Missing custom header")
# 注意:此方法的安全性依赖于 CORS 配置正确
# 如果 CORS 允许任意源发送自定义头,此防御无效// 前端:所有 AJAX 请求添加自定义头
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
fetch('/api/data', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});框架内置防御
Django
# Django 内置 CSRF 中间件
MIDDLEWARE = [
'django.middleware.csrf.CsrfViewMiddleware', # 默认启用
]
# 模板中使用
# <form method="POST">
# {% csrf_token %}
# <input type="text" name="username">
# </form>
# AJAX 请求
# Django 的 CSRF token 通过 csrftoken Cookie 提供
# 前端读取并放入 X-CSRFToken 头
# 对特定视图豁免(如 webhook 接收)
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def webhook_handler(request):
# 需要其他认证方式(如签名验证)
passSpring Security
// Spring Security 默认启用 CSRF 保护
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.ignoringRequestMatchers("/api/webhook/**") // webhook 豁免
);
return http.build();
}
}防御策略选择
最佳实践
- 所有 Cookie 设置
SameSite=Lax作为基线防御 - GET 请求必须是安全的(幂等),不执行状态变更操作
- 使用框架内置的 CSRF 保护,不要自己实现
- CSRF Token 使用恒定时间比较,防止时序攻击
- API 优先使用 Bearer Token 认证,避免 Cookie 带来的 CSRF 风险
- 验证 Origin 和 Referer 头作为额外的防御层
- 关键操作增加二次确认(如密码确认、短信验证码)
- CORS 配置不要过于宽松,CORS 错误配置会削弱 CSRF 防御
贡献者
更新日志
2026/3/14 13:09
查看所有更新日志
9f6c2-feat: organize wiki content and refresh site setup于