CSRF漏洞详解
基础原理
CSRF(Cross-Site Request Forgery,跨站请求伪造),也被称为One Click Attack或者Session Riding,是一种对网站的恶意利用。尽管听起来像跨站脚本攻击(XSS),但它们非常不同:XSS利用站点内的信任用户,而CSRF则通过伪装成受信任用户请求受信任的网站。
漏洞定义
CSRF是一种Web安全漏洞,攻击者诱使已认证用户在不知情、未授权的情况下,以该用户的身份执行非本意的操作。它继承了受害者的身份和权限,以代表受害者执行不需要的功能。
漏洞成因
CSRF漏洞的形成主要有以下几个原因:
- Web应用程序依赖浏览器的Cookie进行身份认证
- 浏览器请求会自动包含与站点关联的凭证(如Cookie、IP地址、Windows域凭证等)
- 应用程序没有对请求的来源进行验证
- 缺少防CSRF机制(如CSRF Token)
与XSS的区别
CSRF和XSS虽然都是跨站攻击,但它们的原理和利用方式完全不同:
| 特性 | CSRF | XSS |
|---|
| 攻击方式 | 伪造用户请求 | 注入恶意脚本 |
| 信任关系 | 利用浏览器对网站的信任 | 利用网站对用户的信任 |
| 防御重点 | 验证请求来源 | 过滤用户输入 |
| 执行位置 | 浏览器自动发送 | 在受害者浏览器中执行 |
漏洞危害
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攻击的典型流程如下:
- 用户登录目标网站(如bank.com),浏览器保存Cookie
- 用户访问攻击者构造的恶意网站(如evil.com)
- 恶意网站中包含指向目标网站的请求
- 浏览器自动发送目标网站的Cookie
- 服务器认为这是用户的合法请求,执行相应操作
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 src="http://target.com/admin/delete?id=1" style="display:none"></iframe>
<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
| 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
|
setInterval(function() { var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://attacker.com/admin/delete?id=1', true); xhr.send(); }, 1000);
|
漏洞检测
1. 手动检测
寻找可能存在CSRF漏洞的功能点:
- 用户设置修改(密码、邮箱、手机号)
- 敏感操作(删除、转账、提现)
- 权限提升(添加管理员、修改角色)
- 数据操作(修改订单、删除记录)
检测步骤:
- 登录目标网站
- 找到敏感操作的功能点
- 使用Burp Suite抓取请求
- 复制请求,在浏览器中直接访问
- 如果操作成功,则存在CSRF漏洞
2. 自动化工具
使用CSRFTester等工具进行自动化检测:
1 2 3 4 5 6
| java -jar CSRFTester.jar
|
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();
function generate_csrf_token() { if (empty($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } return $_SESSION['csrf_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']; 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; } } 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); } ?>
|
在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) { $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']; 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']; 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();
function generate_csrf_token() { $token = bin2hex(random_bytes(32)); $_SESSION['csrf_token'] = $token; $_SESSION['csrf_token_time'] = time(); return $token; }
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); }
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; }
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
| <form method="POST" action="/profile"> @csrf <input type="text" name="name"> <button type="submit">提交</button> </form>
|
Django
1 2 3 4 5 6
| <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
| <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防护的重要性:
- 所有表单必须包含CSRF Token
- 所有AJAX请求必须包含CSRF Token
- 敏感操作必须进行二次确认
- 定期进行安全测试和代码审计
总结
CSRF漏洞是一种常见的Web安全漏洞,可以导致用户在不知情的情况下执行恶意操作。防御CSRF漏洞需要从多个层面入手:
- 使用CSRF Token进行请求验证
- 设置Cookie的SameSite属性
- 验证请求的来源(Referer/Origin)
- 对敏感操作进行二次确认
- 限制请求方法
- 使用成熟的安全框架
- 建立安全编码规范
只有综合运用多种防御措施,才能有效防范CSRF漏洞的攻击。同时,开发人员应该提高安全意识,在开发过程中时刻考虑CSRF防护,避免引入此类漏洞。