2016年,我负责的一个项目,差点遭到CSRF攻击,幸好,及时发现,并且做了防御。
事情是这样的,我们的项目,有一个,修改用户密码的接口,没有做CSRF防御。有一天,安全扫描的时候,发现了这个漏洞,报告说,这个接口存在CSRF风险,攻击者可以构造一个恶意页面,诱导已登录的用户访问,就能在用户不知情的情况下,修改用户的密码。
当时,吓出一身冷汗,幸好,是安全扫描发现的,不是真的被攻击了。然后,赶紧,给这个接口,加上了CSRF防御,并且,全面排查了项目里的其他接口,确保都有CSRF防御。
这件事,让我对CSRF攻击有了更深刻的认识,也让我开始思考,如何设计一套高可用、高并发的CSRF攻击防御架构,而不是零散地、临时地加防御。
今天,就来聊聊CSRF攻击防御的架构设计。
一、什么是CSRF攻击
先简单介绍一下什么是CSRF攻击。
CSRF,全称Cross-Site Request Forgery,跨站请求伪造,是一种常见的Web安全攻击。
CSRF攻击的原理是,攻击者构造一个恶意页面,诱导已登录的用户访问这个恶意页面,恶意页面会自动向目标网站发送请求,因为用户已经登录了目标网站,浏览器会自动带上用户的Cookie,所以目标网站会认为这个请求是用户本人发出的,从而执行请求。
举个例子,假设银行网站有一个转账接口,https://bank.com/transfer?to=attacker&amount=1000,用户登录了银行网站,Cookie里有登录态。攻击者构造一个恶意页面,里面有一个img标签,src指向这个转账接口,然后诱导用户访问这个恶意页面。用户访问后,浏览器会自动加载img,向银行网站发送转账请求,并且带上用户的Cookie,银行网站认为是用户本人操作,就执行了转账,把1000块钱转给了攻击者。
这就是CSRF攻击,用户在不知情的情况下,被攻击者利用,执行了非预期的操作。
CSRF攻击的危害很大,可以修改用户信息、转账、发帖、删数据等等,只要是用户有权限做的操作,都可能被CSRF攻击利用。
二、CSRF攻击的条件
CSRF攻击需要满足几个条件:
- 用户已经登录了目标网站,浏览器里有目标网站的登录Cookie
- 用户访问了攻击者构造的恶意页面
- 恶意页面向目标网站发送了请求,浏览器自动带上了Cookie
- 目标网站没有验证请求的来源,认为是用户本人发出的
只要满足这几个条件,CSRF攻击就能成功。
CSRF攻击的特点是,攻击者不能获取用户的Cookie,也不能看到响应的内容,只能诱导用户发送请求,执行操作。所以,CSRF攻击主要用于执行操作,而不是窃取数据。
三、常见的CSRF防御方法
常见的CSRF防御方法有以下几种:
1. 验证Referer
验证请求的Referer头,看请求是不是从本站发出的。如果Referer不是本站,就拒绝请求。
这种方法简单,但是不可靠,因为Referer可以被伪造,而且有些浏览器或代理会去掉Referer,导致误杀。
2. SameSite Cookie
设置Cookie的SameSite属性,限制Cookie在跨站请求中不会被发送。
SameSite有三个值:
- Strict:严格模式,跨站请求完全不发送Cookie
- Lax:宽松模式,导航到目标站点的GET请求会发送Cookie,其他跨站请求不发送
- None:不限制,但是需要配合Secure属性
SameSite Cookie是一种比较新的防御方法,现代浏览器都支持,但是老浏览器不支持,而且有些场景(比如跨站登录)需要None模式,会降低安全性。
3. CSRF Token
CSRF Token是最常用、最可靠的CSRF防御方法。
原理是,服务端生成一个随机的Token,存在用户的Session里,然后在表单或请求中带上这个Token,服务端验证请求中的Token和Session里的Token是否一致,如果不一致就拒绝请求。
因为攻击者不能获取用户的Token(Token存在Session里,或者在页面里,跨域不能获取),所以攻击者构造的请求不能带上正确的Token,从而防御CSRF攻击。
CSRF Token的优点是可靠,缺点是需要在每个请求中带上Token,实现起来比较麻烦,而且在高并发、分布式环境下,Session共享是个问题。
4. 双重提交Cookie
双重提交Cookie是CSRF Token的一种变体,不需要服务端存储Token。
原理是,服务端生成一个随机的Token,存在Cookie里,然后要求请求中(表单字段或请求头)也带上这个Token,服务端验证Cookie里的Token和请求里的Token是否一致。
因为攻击者不能读取或修改跨域Cookie,所以攻击者构造的请求不能带上正确的Token(虽然Cookie会自动带上,但是请求里的Token攻击者不知道),从而防御CSRF攻击。
双重提交Cookie的优点是不需要服务端存储,适合分布式、高并发环境,缺点是安全性比CSRF Token稍低(如果有子域XSS漏洞,可能被攻破)。
5. 自定义请求头
对于AJAX请求,可以要求带上自定义的请求头,比如X-Requested-With: XMLHttpRequest,或者自定义的Token头。
因为跨域请求不能自定义请求头(除非CORS允许),所以攻击者构造的跨域请求不能带上自定义请求头,从而防御CSRF攻击。
这种方法适合纯AJAX的API,但是不适合表单提交。
四、高可用高并发架构设计
了解了常见的防御方法,接下来聊聊如何设计一套高可用、高并发的CSRF防御架构。
在高可用、高并发的场景下,CSRF防御需要考虑以下几个问题:
- 性能:不能因为CSRF防御影响系统性能,不能成为瓶颈
- 可用性:不能因为CSRF防御导致正常请求被误杀
- 分布式:在分布式、多服务器环境下,Token存储和验证要一致
- 扩展性:能方便地扩展,支持更多的应用和接口
- 易用性:对开发者友好,不需要每个接口都手动加防御
基于这些考虑,我设计的CSRF防御架构如下:
架构分层:
- 接入层(Nginx):基础防护,验证Referer,拦截明显的恶意请求
- 应用层(PHP):核心防御,CSRF Token生成和验证,双重提交Cookie
- 存储层(Redis):Token存储,分布式共享,高性能
- 配置层:统一配置,黑白名单,灵活控制
具体设计:
1. Token生成
用户登录后,或者首次访问时,生成一个CSRF Token,Token是随机字符串(比如32位的随机字符串),存在Redis里,Key是csrf:token:{userid}或者csrf:token:{sessionid},Value是Token,过期时间和Session一致(比如2小时)。
同时,把Token设置到Cookie里(用于双重提交Cookie),Cookie的名字是csrf_token,HttpOnly=false(因为前端需要读取),Secure=true(HTTPS环境),SameSite=Lax。
Token可以在用户整个会话期间使用,也可以每次请求后刷新(更安全,但是实现复杂,而且会导致后退按钮问题)。建议会话级Token,兼顾安全和易用。
2. Token传递
前端需要在每个请求中带上CSRF Token:
- 表单提交:在表单里加一个隐藏字段
<input type="hidden" name="csrf_token" value="{token}"> - AJAX请求:在请求头里加
X-CSRF-Token: {token},或者在请求体里加csrf_token字段
前端可以从Cookie里读取Token(因为HttpOnly=false),然后自动加到每个请求里,不需要开发者手动处理。
3. Token验证
服务端在处理请求前,验证CSRF Token:
- 对于GET请求:默认不验证(GET请求应该是安全的,不修改数据),但是可以配置验证
- 对于POST/PUT/DELETE请求:必须验证Token
- 验证逻辑:从请求中获取Token(表单字段、请求头、请求体),和Redis里存储的Token比较,如果一致就通过,不一致就拒绝(返回403)
验证逻辑可以做成中间件或钩子,自动执行,不需要每个接口手动处理。
4. 黑白名单
支持黑白名单配置:
- 白名单:某些接口不需要CSRF验证(比如登录接口、回调接口、开放API),配置白名单,跳过验证
- 黑名单:某些接口需要更严格的验证(比如修改密码、转账、删除数据),配置黑名单,要求每次请求刷新Token,或者二次验证
黑白名单可以按URL正则配置,灵活控制。
5. 分布式支持
Token存储在Redis里,所有服务器共享,支持分布式部署。Redis高性能,能支持高并发的Token读写。
Redis可以用集群模式,保证高可用,避免单点故障。
6. 降级策略
如果Redis故障,Token验证无法进行,需要有降级策略:
- 降级为双重提交Cookie验证(只验证Cookie里的Token和请求里的Token是否一致,不需要Redis)
- 或者临时关闭CSRF验证(不推荐,但是比服务不可用好)
- 同时告警,通知运维人员修复Redis
降级策略可以保证在Redis故障时,服务仍然可用,同时尽量保证安全。
五、具体实现
接下来聊聊具体的实现,以PHP为例。
1. Token生成函数
function generateCsrfToken($userId) {
$token = bin2hex(random_bytes(16)); // 32位随机字符串
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$key = "csrf:token:{$userId}";
$redis->setex($key, 7200, $token); // 2小时过期
// 设置Cookie
setcookie('csrf_token', $token, time() + 7200, '/', '', true, false);
return $token;
}2. Token验证中间件
function verifyCsrfToken($userId) {
// 获取请求中的Token
$token = $_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
if (empty($token)) {
return false;
}
// 从Redis获取存储的Token
try {
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$key = "csrf:token:{$userId}";
$storedToken = $redis->get($key);
} catch (Exception $e) {
// Redis故障,降级为双重提交Cookie验证
$cookieToken = $_COOKIE['csrf_token'] ?? '';
return hash_equals($cookieToken, $token);
}
if (empty($storedToken)) {
return false;
}
// 使用hash_equals防止时序攻击
return hash_equals($storedToken, $token);
}3. 自动验证钩子
在应用入口处,自动执行CSRF验证:
// CSRF验证中间件
function csrfMiddleware() {
$method = $_SERVER['REQUEST_METHOD'];
// GET请求默认不验证
if ($method === 'GET') {
return true;
}
// 检查白名单
$uri = $_SERVER['REQUEST_URI'];
$whitelist = ['/login', '/callback', '/api/open'];
foreach ($whitelist as $pattern) {
if (strpos($uri, $pattern) === 0) {
return true;
}
}
// 验证Token
$userId = getCurrentUserId(); // 获取当前登录用户ID
if (!verifyCsrfToken($userId)) {
http_response_code(403);
echo 'CSRF验证失败';
exit;
}
return true;
}
// 在应用入口调用
csrfMiddleware();4. 前端自动添加Token
前端可以用JavaScript自动给每个AJAX请求加上CSRF Token:
// 从Cookie读取Token
function getCsrfToken() {
const match = document.cookie.match(/csrf_token=([^;]+)/);
return match ? match[1] : '';
}
// 给所有AJAX请求加上Token
const originalOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url) {
originalOpen.apply(this, arguments);
if (method !== 'GET') {
this.setRequestHeader('X-CSRF-Token', getCsrfToken());
}
};
// 给fetch加上Token
const originalFetch = window.fetch;
window.fetch = function(url, options = {}) {
if (options.method && options.method !== 'GET') {
options.headers = options.headers || {};
options.headers['X-CSRF-Token'] = getCsrfToken();
}
return originalFetch.apply(this, arguments);
};这样,前端所有的AJAX请求都会自动带上CSRF Token,不需要开发者手动处理。
六、踩坑经验
在实现CSRF防御架构的过程中,我踩了一些坑,分享一下。
1. Token过期问题
Token有过期时间,如果用户打开页面很久没操作,Token过期了,提交表单会验证失败。
解决方法:
- Token过期时间设置长一些(比如2小时),和Session一致
- 页面上的Token过期后,前端可以异步刷新Token(调用一个接口获取新Token)
- 验证失败时,返回友好的提示,让用户刷新页面
2. 多标签页问题
用户打开多个标签页,如果每个标签页生成不同的Token,会导致后面的标签页覆盖前面的Token,前面的标签页提交失败。
解决方法:
- 使用会话级Token,整个会话共用一个Token,不每次刷新
- 或者支持多个Token同时有效(Redis里存一个Token列表)
3. 文件上传问题
文件上传的表单,CSRF Token怎么传递?
解决方法:
- 在表单里加隐藏字段csrf_token,和文件一起提交
- 或者在URL参数里加csrf_token(不推荐,Token会出现在URL里)
- 验证的时候从POST里获取Token(文件上传也是POST)
4. CORS跨域问题
如果有跨域请求(比如前后端分离,前端域名和后端域名不同),CSRF防御需要注意CORS配置。
解决方法:
- 跨域请求需要配置CORS,允许跨域
- 但是CSRF Token的Cookie需要设置正确的Domain,确保跨域请求能带上
- 或者不使用Cookie传递Token,而是登录后接口返回Token,前端存在localStorage里,请求时带上
5. 误杀问题
CSRF验证可能会误杀正常请求,比如:
- 某些客户端不支持Cookie(比如某些API客户端)
- 某些代理会去掉请求头
- 白名单配置不正确
解决方法:
- 完善白名单配置,不需要验证的接口加入白名单
- 验证失败时记录日志,分析误杀原因
- 提供多种Token传递方式(表单字段、请求头、请求体),兼容不同客户端
6. 性能问题
每个请求都要验证CSRF Token,都要访问Redis,可能会影响性能。
解决方法:
- Redis本身性能很高,能支持高并发
- 可以在应用层缓存Token(比如存在PHP进程的内存里,或者APCu),减少Redis访问
- GET请求不验证,减少验证次数
- 白名单接口不验证
七、最佳实践
最后总结一些CSRF防御的最佳实践。
1. 纵深防御
不要只依赖一种防御方法,采用纵深防御,多种方法结合:
- Nginx层验证Referer(基础防护)
- SameSite Cookie(浏览器层防护)
- CSRF Token(核心防护)
- 自定义请求头(AJAX请求防护)
多种方法结合,即使一种方法被攻破,还有其他方法防护。
2. GET请求安全
确保GET请求是安全的,不修改数据,只读取数据。这样即使GET请求被CSRF攻击,也不会造成危害。
修改数据的操作(增删改)必须用POST/PUT/DELETE,并且必须验证CSRF Token。
3. Token安全
- Token必须是随机的,不可预测的(用randombytes或opensslrandompseudobytes)
- Token有过期时间,不要永久有效
- Token通过HTTPS传输,设置Secure属性
- 验证Token用hash_equals,防止时序攻击
- 不要把Token放在URL里(会出现在日志、Referer里)
4. 最小权限
用户权限遵循最小权限原则,即使被CSRF攻击,造成的危害也有限。
比如,普通用户不能管理后台,不能修改其他用户信息,即使被CSRF攻击,也只能修改自己的信息,危害有限。
5. 安全监控
- 记录CSRF验证失败的日志,监控异常
- 定期安全扫描,检查是否有CSRF漏洞
- 关注安全公告,及时更新框架和库
- 定期安全审计,检查CSRF防御是否有效
6. 开发者教育
- 对开发者进行安全培训,了解CSRF攻击的原理和危害
- 制定安全规范,明确哪些接口需要CSRF防御
- Code Review时检查CSRF防御
- 提供统一的CSRF防御组件,开发者不需要自己实现,避免出错
八、写在最后
CSRF攻击防御架构设计:高可用高并发。
CSRF是一种常见的Web安全攻击,危害很大,但是只要做好防御,是完全可以防御的。
防御CSRF攻击,不能零散地、临时地加防御,而应该在架构设计层面就考虑到,设计一套统一的、高可用的、高并发的CSRF防御架构,自动执行,对开发者透明,这样才能真正有效地防御CSRF攻击。
安全不是一蹴而就的,需要持续关注,不断优化,才能真正防御各种攻击。
希望我的这些架构设计和踩坑经验,能对大家有所帮助,让大家的项目更安全。
最后,用一句话结尾:
"安全是系统的属性,不是功能。它应该融入架构的每一层,而不是事后打补丁。"
愿大家都能构建安全、可靠的Web应用。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录