4、CSRF攻击与防御:SameSite Cookie、Token验证、Referer校验
4.1 什么是CSRF?我踩过的坑
CSRF,全称叫跨站请求伪造。说白了就是:攻击者利用你的登录状态,在你不知情的情况下,替你去干坏事。
我记得刚工作那会儿,有个同事负责的论坛系统出了个问题。用户A在论坛发帖,莫名其妙就变成了管理员,还能删帖封号。查了半天,原来是CSRF攻击。攻击者在另一个网站上放了个隐藏表单,自动提交了一个「修改用户权限」的请求。因为用户A的登录Cookie还在有效期内,服务器就傻乎乎地信了。
嗯,这就是CSRF最典型的场景。你想想看,你登录了银行网站,然后不小心点开了某个钓鱼页面。那个页面悄悄发起一个转账请求,你的Cookie自动带上去了,银行服务器一看「哦,是本人操作」,钱就转走了。
4.2 SameSite Cookie:浏览器自带的防护盾
SameSite是浏览器提供的一个Cookie属性。我个人习惯把它当作第一道防线,因为它不需要改后端代码,配置一下就行。
SameSite有三个值:
| 值 | 行为 | 适用场景 |
|---|---|---|
Strict |
完全禁止跨站携带Cookie | 银行、支付等敏感操作 |
Lax |
允许部分安全请求(GET)携带Cookie | 大多数网站,默认值 |
None |
允许所有跨站请求携带Cookie | 需要跨站共享Cookie的场景 |
我在项目中遇到过一个问题:用了SameSite=Strict之后,用户从第三方网站点击链接跳转到我们网站,登录状态丢了。因为跳转是GET请求,但Strict模式下连GET都不带Cookie。后来改成了Lax,既保证了安全,又不影响用户体验。
// 服务端设置Cookie
Set-Cookie: session_id=abc123; SameSite=Lax; Secure; HttpOnly
// 或者用Node.js的express-session
app.use(session({
cookie: {
sameSite: 'lax',
secure: true
}
}))
4.3 Token验证:最常用的防御手段
Token验证,说白了就是在表单里藏一个随机字符串。服务器生成这个Token,塞到页面里。用户提交表单时,Token一起发回来。服务器比对一下,对得上就放行,对不上就拒绝。
为什么Token能防CSRF?因为攻击者拿不到你的Token。Token是服务器生成、嵌入到当前页面里的。攻击者的页面没法读取你页面里的Token(同源策略限制),所以伪造的请求里Token要么为空,要么是错的。
我曾经在一个电商项目中用过这个方案。具体做法是这样的:
// 后端生成Token(Node.js示例)
const crypto = require('crypto');
function generateCSRFToken(req) {
const token = crypto.randomBytes(32).toString('hex');
req.session.csrfToken = token;
return token;
}
// 渲染页面时把Token塞到表单里
app.get('/transfer', (req, res) => {
const token = generateCSRFToken(req);
res.render('transfer', { csrfToken: token });
});
// 验证Token
app.post('/transfer', (req, res) => {
if (req.body.csrfToken !== req.session.csrfToken) {
return res.status(403).send('CSRF Token验证失败');
}
// 执行转账逻辑
});
前端表单里这样写:
<form action="/transfer" method="POST">
<input type="hidden" name="csrfToken" value="{{ csrfToken }}">
<input type="text" name="amount">
<button type="submit">转账</button>
</form>
4.4 Referer校验:简单但不够完美
Referer校验,就是检查请求头里的Referer字段。如果Referer不是我们自己的域名,就拒绝请求。
这个方法简单粗暴,但有两个问题:
- Referer可能为空:用户通过书签、邮件客户端、某些浏览器插件访问时,Referer可能不发送。
- Referer可以被篡改:虽然浏览器不允许JS修改Referer,但攻击者可以用Flash、Java Applet等老技术伪造Referer。
我在项目中一般把Referer校验作为辅助手段,不单独依赖它。比如这样:
// 简单的Referer校验中间件
function refererCheck(req, res, next) {
const referer = req.headers.referer || '';
const allowedDomains = ['https://mysite.com', 'https://www.mysite.com'];
const isValid = allowedDomains.some(domain => referer.startsWith(domain));
if (!isValid) {
return res.status(403).send('非法来源');
}
next();
}
// 只对敏感接口使用
app.post('/transfer', refererCheck, (req, res) => {
// 转账逻辑
});
4.5 三种方案怎么选?我的建议
说实话,没有银弹。我个人的做法是分层防御:
- 第一层:SameSite Cookie —— 浏览器层面兜底,配置简单,覆盖大部分场景。
- 第二层:Token验证 —— 核心防御,对关键接口(转账、改密、发帖)强制校验。
- 第三层:Referer校验 —— 辅助手段,作为日志记录和告警依据。
你想想看,三层都上了,攻击者得同时绕过三个关卡,难度就大多了。
- 新项目:直接用SameSite Lax + Token验证,够用了
- 老项目改造:先加SameSite,再逐步加Token
- 高安全场景(金融、政务):三层全上,再加验证码
嗯,这就是我对CSRF防御的理解。说白了,就是让攻击者「借不了你的身份,发不了你的请求」。记住这个核心,你就知道该怎么防了。