CSRF漏洞详解

基础原理

CSRF(Cross-Site Request Forgery,跨站请求伪造),也被称为One Click Attack或者Session Riding,是一种对网站的恶意利用。尽管听起来像跨站脚本攻击(XSS),但它们非常不同:XSS利用站点内的信任用户,而CSRF则通过伪装成受信任用户请求受信任的网站。

漏洞定义

CSRF是一种Web安全漏洞,攻击者诱使已认证用户在不知情、未授权的情况下,以该用户的身份执行非本意的操作。它继承了受害者的身份和权限,以代表受害者执行不需要的功能。

漏洞成因

CSRF漏洞的形成主要有以下几个原因:

  1. Web应用程序依赖浏览器的Cookie进行身份认证
  2. 浏览器请求会自动包含与站点关联的凭证(如Cookie、IP地址、Windows域凭证等)
  3. 应用程序没有对请求的来源进行验证
  4. 缺少防CSRF机制(如CSRF Token)

与XSS的区别

CSRF和XSS虽然都是跨站攻击,但它们的原理和利用方式完全不同:

特性CSRFXSS
攻击方式伪造用户请求注入恶意脚本
信任关系利用浏览器对网站的信任利用网站对用户的信任
防御重点验证请求来源过滤用户输入
执行位置浏览器自动发送在受害者浏览器中执行

漏洞危害

CSRF漏洞的危害主要体现在以下几个方面:

1. 敏感操作执行

攻击者可以诱导用户执行各种敏感操作:

1
2
3
4
5
6
7
8
<!-- 修改密码 -->
<img src="http://target.com/change_password?new=123456&confirm=123456">

<!-- 添加管理员 -->
<img src="http://target.com/admin/add_user?username=attacker&role=admin">

<!-- 转账 -->
<img src="http://bank.com/transfer?to=attacker&amount=10000">

2. 数据篡改

攻击者可以修改用户的数据:

1
2
3
4
5
<!-- 修改邮箱 -->
<img src="http://target.com/profile/update?email=attacker@evil.com">

<!-- 修改收货地址 -->
<img src="http://target.com/order/update_address?address=attacker_address">

3. 账户劫持

攻击者可以劫持用户账户:

1
2
3
4
5
<!-- 绑定攻击者的手机号 -->
<img src="http://target.com/account/bind_phone?phone=13800138000">

<!-- 修改安全问题 -->
<img src="http://target.com/account/security?question=hacker&answer=hacker">

攻击原理

浏览器Cookie机制

CSRF攻击的核心在于浏览器的Cookie机制。当用户登录网站后,浏览器会保存该网站的Cookie。之后用户访问该网站的任何页面时,浏览器都会自动发送Cookie。

1
2
3
GET /profile HTTP/1.1
Host: target.com
Cookie: session_id=abc123; user_id=456

服务器通过Cookie识别用户身份,但无法区分请求是由用户主动发起还是由攻击者伪造的。

攻击流程

CSRF攻击的典型流程如下:

  1. 用户登录目标网站(如bank.com),浏览器保存Cookie
  2. 用户访问攻击者构造的恶意网站(如evil.com)
  3. 恶意网站中包含指向目标网站的请求
  4. 浏览器自动发送目标网站的Cookie
  5. 服务器认为这是用户的合法请求,执行相应操作
1
2
3
4
5
6
7
8
9
用户浏览器
|
|--登录bank.com--> 保存Cookie
|
|--访问evil.com--> 加载恶意页面
| |
| |--自动请求bank.com--> 携带Cookie
| |
| |--执行恶意操作

常见攻击方式

1. GET请求攻击

最简单的CSRF攻击方式,利用GET请求的自动执行特性:

1
2
3
4
5
6
7
8
<!-- 隐藏的图片标签 -->
<img src="http://target.com/change_password?new=123456" style="display:none">

<!-- 隐藏的iframe -->
<iframe src="http://target.com/admin/delete?id=1" style="display:none"></iframe>

<!-- script标签 -->
<script src="http://target.com/transfer?to=attacker&amount=10000"></script>

2. POST请求攻击

对于POST请求,攻击者可以使用自动提交的表单:

1
2
3
4
5
6
7
8
<form action="http://target.com/change_password" method="POST" id="csrf-form">
<input type="hidden" name="new_password" value="123456">
<input type="hidden" name="confirm_password" value="123456">
</form>

<script>
document.getElementById('csrf-form').submit();
</script>

3. 链接钓鱼攻击

诱导用户点击恶意链接:

1
<a href="http://target.com/admin/delete?id=1">点击领取红包</a>

用户点击后,浏览器会自动发送请求。

4. 高级攻击技术

4.1 结合XSS

攻击者可以先利用XSS漏洞获取CSRF Token,然后发起CSRF攻击:

1
2
3
4
5
6
// XSS Payload
var token = document.querySelector('input[name="csrf_token"]').value;
var xhr = new XMLHttpRequest();
xhr.open('POST', 'http://target.com/change_password', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('new_password=123456&csrf_token=' + token);

4.2 点击劫持

使用透明iframe覆盖页面,诱导用户点击:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<style>
.clickjacking {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
z-index: 9999;
}
</style>

<iframe src="http://target.com/delete_account" class="clickjacking"></iframe>
<button>点击领取奖品</button>

4.3 DNS Rebinding

利用DNS重绑定绕过同源策略:

1
2
3
4
5
6
7
8
9
// 攻击者控制的域名
// 第一次解析:attacker.com -> 攻击者服务器IP
// 第二次解析:attacker.com -> 目标网站IP

setInterval(function() {
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://attacker.com/admin/delete?id=1', true);
xhr.send();
}, 1000);

漏洞检测

1. 手动检测

寻找可能存在CSRF漏洞的功能点:

  • 用户设置修改(密码、邮箱、手机号)
  • 敏感操作(删除、转账、提现)
  • 权限提升(添加管理员、修改角色)
  • 数据操作(修改订单、删除记录)

检测步骤:

  1. 登录目标网站
  2. 找到敏感操作的功能点
  3. 使用Burp Suite抓取请求
  4. 复制请求,在浏览器中直接访问
  5. 如果操作成功,则存在CSRF漏洞

2. 自动化工具

使用CSRFTester等工具进行自动化检测:

1
2
3
4
5
6
# CSRFTester
java -jar CSRFTester.jar

# 配置浏览器代理
# 访问目标网站,执行各种操作
# 工具自动分析请求,生成CSRF测试页面

3. Burp Suite插件

使用CSRF相关的Burp Suite插件:

  • CSRF Token Tracker
  • CSRF Scanner

防御措施

1. CSRF Token

最有效的CSRF防御方法是在表单中添加随机生成的Token:

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
<?php
session_start();

// 生成Token
function generate_csrf_token() {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}

// 验证Token
function verify_csrf_token($token) {
return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
}

// 表单生成
$csrf_token = generate_csrf_token();
?>

<form action="/change_password" method="POST">
<input type="hidden" name="csrf_token" value="<?php echo $csrf_token; ?>">
<input type="password" name="new_password" placeholder="新密码">
<input type="submit" value="修改密码">
</form>

<?php
// 表单处理
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!verify_csrf_token($_POST['csrf_token'])) {
die('CSRF Token验证失败');
}

$new_password = $_POST['new_password'];
// 处理密码修改
}
?>

2. SameSite Cookie属性

设置Cookie的SameSite属性,限制Cookie的发送范围:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
session_set_cookie_params([
'lifetime' => 3600,
'path' => '/',
'domain' => 'example.com',
'secure' => true,
'httponly' => true,
'samesite' => 'Strict' // 或 'Lax'
]);

session_start();
?>

SameSite属性的三种值:

  • Strict:最严格,只在同站请求中发送Cookie
  • Lax:推荐值,允许部分跨站请求发送Cookie(如导航链接)
  • None:不限制,需要配合secure属性使用

3. 验证Referer和Origin

检查请求的来源:

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
<?php
function verify_request_origin() {
$allowed_domains = ['https://example.com', 'https://www.example.com'];

// 检查Referer
if (isset($_SERVER['HTTP_REFERER'])) {
$referer = parse_url($_SERVER['HTTP_REFERER']);
$referer_domain = $referer['scheme'] . '://' . $referer['host'];

if (in_array($referer_domain, $allowed_domains)) {
return true;
}
}

// 检查Origin
if (isset($_SERVER['HTTP_ORIGIN'])) {
if (in_array($_SERVER['HTTP_ORIGIN'], $allowed_domains)) {
return true;
}
}

return false;
}

if (!verify_request_origin()) {
die('非法请求来源');
}
?>

4. 二次确认

对敏感操作进行二次确认:

1
2
3
4
5
6
<form action="/delete_account" method="POST">
<input type="hidden" name="csrf_token" value="<?php echo $csrf_token; ?>">
<input type="checkbox" name="confirm" id="confirm">
<label for="confirm">我确认要删除账户</label>
<input type="submit" value="删除账户">
</form>
1
2
3
4
5
6
7
8
9
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!isset($_POST['confirm']) || $_POST['confirm'] !== 'on') {
die('请确认操作');
}

// 执行删除操作
}
?>

5. 限制请求方法

对敏感操作只允许POST请求:

1
2
3
4
5
6
7
<?php
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
die('只允许POST请求');
}

// 处理敏感操作
?>

6. 短期有效的Token

设置Token的过期时间:

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
<?php
session_start();

function generate_csrf_token() {
$token = bin2hex(random_bytes(32));
$_SESSION['csrf_token'] = $token;
$_SESSION['csrf_token_time'] = time();
return $token;
}

function verify_csrf_token($token, $max_age = 3600) {
if (!isset($_SESSION['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $token)) {
return false;
}

if (!isset($_SESSION['csrf_token_time'])) {
return false;
}

if (time() - $_SESSION['csrf_token_time'] > $max_age) {
return false;
}

return true;
}
?>

7. 双重Cookie提交

在Cookie和表单中都包含Token,服务器进行双重验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
session_start();

function generate_csrf_token() {
$token = bin2hex(random_bytes(32));
$_SESSION['csrf_token'] = $token;
setcookie('csrf_token', $token, time() + 3600, '/', '', true, true);
return $token;
}

function verify_csrf_token($token) {
$cookie_token = $_COOKIE['csrf_token'] ?? '';
$session_token = $_SESSION['csrf_token'] ?? '';

return hash_equals($token, $cookie_token) && hash_equals($token, $session_token);
}
?>

8. 自定义Header

在AJAX请求中添加自定义Header:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 前端代码
function changePassword(newPassword) {
var xhr = new XMLHttpRequest();
xhr.open('POST', '/change_password', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.setRequestHeader('X-CSRF-Token', getCsrfToken());

xhr.onload = function() {
if (xhr.status === 200) {
console.log('密码修改成功');
}
};

xhr.send(JSON.stringify({
new_password: newPassword
}));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
// 后端验证
function verify_csrf_token() {
$headers = getallheaders();
$token = $headers['X-CSRF-Token'] ?? '';

return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
}

if (!verify_csrf_token()) {
header('HTTP/1.1 403 Forbidden');
exit;
}
?>

实战案例

案例1:修改密码CSRF

漏洞代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
session_start();

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$new_password = $_POST['new_password'];
$confirm_password = $_POST['confirm_password'];

if ($new_password === $confirm_password) {
// 直接修改密码,没有CSRF保护
$user_id = $_SESSION['user_id'];
update_password($user_id, $new_password);
echo '密码修改成功';
}
}
?>

<form action="/change_password" method="POST">
<input type="password" name="new_password" placeholder="新密码">
<input type="password" name="confirm_password" placeholder="确认密码">
<input type="submit" value="修改密码">
</form>

攻击Payload:

1
<img src="http://target.com/change_password?new_password=123456&confirm_password=123456">

案例2:转账CSRF

漏洞代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
session_start();

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$to_account = $_POST['to_account'];
$amount = $_POST['amount'];

// 没有CSRF保护
transfer($_SESSION['user_id'], $to_account, $amount);
echo '转账成功';
}
?>

<form action="/transfer" method="POST">
<input type="text" name="to_account" placeholder="收款账号">
<input type="number" name="amount" placeholder="金额">
<input type="submit" value="转账">
</form>

攻击Payload:

1
2
3
4
5
6
7
8
<form action="http://bank.com/transfer" method="POST" id="csrf-form">
<input type="hidden" name="to_account" value="attacker_account">
<input type="hidden" name="amount" value="10000">
</form>

<script>
document.getElementById('csrf-form').submit();
</script>

案例3:添加管理员CSRF

漏洞代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
session_start();

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST['username'];
$role = $_POST['role'];

// 没有CSRF保护
add_user($username, $role);
echo '用户添加成功';
}
?>

<form action="/admin/add_user" method="POST">
<input type="text" name="username" placeholder="用户名">
<select name="role">
<option value="user">普通用户</option>
<option value="admin">管理员</option>
</select>
<input type="submit" value="添加用户">
</form>

攻击Payload:

1
<img src="http://target.com/admin/add_user?username=attacker&role=admin">

最佳实践

1. 综合防御

不要依赖单一的防御措施,应该综合使用多种防御手段:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<?php
session_start();

// 1. CSRF Token
function generate_csrf_token() {
$token = bin2hex(random_bytes(32));
$_SESSION['csrf_token'] = $token;
$_SESSION['csrf_token_time'] = time();
return $token;
}

// 2. 验证请求来源
function verify_request_origin() {
$allowed_domains = ['https://example.com'];
$referer = $_SERVER['HTTP_REFERER'] ?? '';
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';

$referer_domain = parse_url($referer, PHP_URL_SCHEME) . '://' . parse_url($referer, PHP_URL_HOST);

return in_array($referer_domain, $allowed_domains) || in_array($origin, $allowed_domains);
}

// 3. 验证Token
function verify_csrf_token($token, $max_age = 3600) {
if (!isset($_SESSION['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $token)) {
return false;
}

if (time() - ($_SESSION['csrf_token_time'] ?? 0) > $max_age) {
return false;
}

return true;
}

// 4. 限制请求方法
function verify_request_method() {
return $_SERVER['REQUEST_METHOD'] === 'POST';
}

// 综合验证
function verify_csrf($token) {
return verify_request_method() &&
verify_request_origin() &&
verify_csrf_token($token);
}

// 使用示例
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!verify_csrf($_POST['csrf_token'])) {
die('CSRF验证失败');
}

// 处理请求
}
?>

2. 安全框架

使用成熟的安全框架,它们通常内置了CSRF防护:

Laravel

1
2
3
4
5
6
// 表单中包含CSRF Token
<form method="POST" action="/profile">
@csrf
<input type="text" name="name">
<button type="submit">提交</button>
</form>

Django

1
2
3
4
5
6
# 表单中包含CSRF Token
<form method="POST" action="/profile/">
{% csrf_token %}
<input type="text" name="name">
<button type="submit">提交</button>
</form>

Spring Security

1
2
3
4
5
6
// 表单中包含CSRF Token
<form method="POST" action="/profile">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
<input type="text" name="name">
<button type="submit">提交</button>
</form>

3. 安全编码规范

建立安全编码规范,确保所有开发人员都了解CSRF防护的重要性:

  1. 所有表单必须包含CSRF Token
  2. 所有AJAX请求必须包含CSRF Token
  3. 敏感操作必须进行二次确认
  4. 定期进行安全测试和代码审计

总结

CSRF漏洞是一种常见的Web安全漏洞,可以导致用户在不知情的情况下执行恶意操作。防御CSRF漏洞需要从多个层面入手:

  1. 使用CSRF Token进行请求验证
  2. 设置Cookie的SameSite属性
  3. 验证请求的来源(Referer/Origin)
  4. 对敏感操作进行二次确认
  5. 限制请求方法
  6. 使用成熟的安全框架
  7. 建立安全编码规范

只有综合运用多种防御措施,才能有效防范CSRF漏洞的攻击。同时,开发人员应该提高安全意识,在开发过程中时刻考虑CSRF防护,避免引入此类漏洞。