4、CSRF攻击与防御:CSRF原理、SameSite Cookie、Token验证、Referer检查
4.1 先聊聊CSRF到底是个啥
CSRF,全称叫跨站请求伪造。名字挺长,说白了就是:攻击者利用你的登录状态,偷偷帮你发请求。
我刚开始接触这个漏洞时,觉得它挺"阴"的。你想想看,用户明明什么都没干,结果钱没了、密码改了、帖子发了。为什么会这样?因为浏览器有个"善良"的机制——它会自动携带Cookie。
举个例子。你登录了银行网站A,浏览器里存了A的Cookie。这时候你手贱点开了攻击者发的钓鱼链接B。B页面里藏了个隐藏表单,自动向A发了个转账请求。浏览器一看,嘿,这是去A的请求,带上Cookie吧!于是请求就带着你的身份发出去了。
嗯,这就是CSRF的核心——利用Cookie自动携带机制,冒充用户操作。
4.2 防御手段一:SameSite Cookie
这是我最喜欢的一种防御方式。因为它简单、有效,而且不需要改后端代码。
SameSite是Cookie的一个属性。它告诉浏览器:这个Cookie只能在同站请求中携带。跨站请求?对不起,不带。
它有3个值:
| 值 | 行为 | 适用场景 |
|---|---|---|
| Strict | 最严格。任何跨站请求都不带Cookie | 银行、支付等敏感操作 |
| Lax | 默认值。GET请求带Cookie,POST不带 | 大部分网站 |
| None | 不限制。跨站也带,但必须配合Secure | 需要跨站共享Cookie的场景 |
我个人习惯把登录态相关的Cookie设为SameSite=Lax。为什么不是Strict?因为Strict太严格了,用户从第三方链接点进来,连登录状态都没有,体验很差。
Secure和HttpOnly。这三个属性一起用,Cookie的安全性会提升一大截。
代码示例:
// Node.js Express 设置 SameSite Cookie
res.cookie('session_id', 'abc123', {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 24 * 60 * 60 * 1000 // 1天
});
我在项目中遇到过一个问题:老版本的Chrome和Safari不支持SameSite。那时候我不得不加个降级方案——检测浏览器版本,不支持的用Token验证兜底。
4.3 防御手段二:Token验证
Token验证是CSRF防御的"老牌选手"。原理很简单:服务器生成一个随机Token,塞到页面里。提交请求时带上这个Token,服务器校验。
攻击者拿不到Token,自然伪造不了请求。
具体做法:
- 用户访问页面时,服务器生成一个随机Token
- Token存在Session里,同时渲染到表单的隐藏域或请求头里
- 用户提交请求时,带上Token
- 服务器比对Token是否一致
代码示例:
// 后端生成Token(Node.js + Express)
const crypto = require('crypto');
// 生成Token
function generateCSRFToken(req) {
const token = crypto.randomBytes(32).toString('hex');
req.session.csrfToken = token;
return token;
}
// 校验Token
function validateCSRFToken(req, res, next) {
const token = req.body._csrf || req.headers['x-csrf-token'];
if (token !== req.session.csrfToken) {
return res.status(403).json({ error: 'CSRF token invalid' });
}
next();
}
<!-- 前端表单 -->
<form action="/transfer" method="POST">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<input type="text" name="amount">
<button type="submit">转账</button>
</form>
Token验证的优点是兼容性好,所有浏览器都支持。缺点是需要后端配合,每个表单都得加隐藏域,稍微有点麻烦。
4.4 防御手段三:Referer检查
Referer检查,说白了就是看请求是从哪个页面发过来的。
原理:正常的请求,Referer应该是本站的域名。攻击者的请求,Referer要么是空,要么是攻击者的域名。
代码示例:
// Node.js 中间件:检查Referer
function checkReferer(req, res, next) {
const referer = req.headers.referer || req.headers.referrer;
const allowedDomain = 'https://yourbank.com';
if (!referer || !referer.startsWith(allowedDomain)) {
return res.status(403).json({ error: 'Invalid referer' });
}
next();
}
这个方法看起来简单,但坑不少。我踩过的坑有:
- Referer可能为空:用户直接输入URL、从书签打开、或者某些隐私模式,Referer都是空的
- Referer可以被篡改:老版本的Flash、某些浏览器插件可以伪造Referer
- HTTPS降级到HTTP时不带Referer:这是浏览器的安全策略
4.5 三种防御手段怎么选
我一般这样搭配:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 新项目 | SameSite=Lax + Token验证 | 双重保险,兼容性好 |
| 老项目改造 | Token验证 + Referer检查 | 改动小,快速上线 |
| 高安全场景(银行、支付) | SameSite=Strict + Token验证 + 二次确认 | 能上的防御全上 |
| API接口 | Token验证(放在请求头里) | API没有Cookie,Token最合适 |
嗯,这里要注意一点:防御不是越多越好。加太多检查,用户体验会变差,维护成本也高。找到适合自己业务的平衡点才是关键。
4.6 避坑指南
我曾经犯过一个低级错误:把CSRF Token存在了Cookie里。你想想看,攻击者虽然拿不到Token的值,但浏览器会自动带上Cookie啊!这不等于白给吗?
所以记住:CSRF Token绝对不能放在Cookie里。要么放Session,要么放页面源码里。
还有一个坑:GET请求也能修改数据。有些开发者图省事,把删除操作写成GET /delete?id=123。结果攻击者用个img标签就搞定了:<img src="https://yourbank.com/delete?id=123">。所以,所有修改数据的操作,必须用POST/PUT/DELETE。
最后总结一下:CSRF防御的核心思路就两个——让攻击者拿不到凭证(Token)、让浏览器不自动带凭证(SameSite)。把这两个想明白了,CSRF就防住了。