4、CSRF攻击与防御:CSRF原理、SameSite Cookie、Token验证、Referer检查

4.1 先聊聊CSRF到底是个啥

CSRF,全称叫跨站请求伪造。名字挺长,说白了就是:攻击者利用你的登录状态,偷偷帮你发请求

我刚开始接触这个漏洞时,觉得它挺"阴"的。你想想看,用户明明什么都没干,结果钱没了、密码改了、帖子发了。为什么会这样?因为浏览器有个"善良"的机制——它会自动携带Cookie。

举个例子。你登录了银行网站A,浏览器里存了A的Cookie。这时候你手贱点开了攻击者发的钓鱼链接B。B页面里藏了个隐藏表单,自动向A发了个转账请求。浏览器一看,嘿,这是去A的请求,带上Cookie吧!于是请求就带着你的身份发出去了。

嗯,这就是CSRF的核心——利用Cookie自动携带机制,冒充用户操作

关键点:CSRF攻击成功的前提是用户已经登录了目标站点,并且浏览器没有对跨站请求做任何限制。

4.2 防御手段一:SameSite Cookie

这是我最喜欢的一种防御方式。因为它简单、有效,而且不需要改后端代码。

SameSite是Cookie的一个属性。它告诉浏览器:这个Cookie只能在同站请求中携带。跨站请求?对不起,不带。

它有3个值:

行为 适用场景
Strict 最严格。任何跨站请求都不带Cookie 银行、支付等敏感操作
Lax 默认值。GET请求带Cookie,POST不带 大部分网站
None 不限制。跨站也带,但必须配合Secure 需要跨站共享Cookie的场景

我个人习惯把登录态相关的Cookie设为SameSite=Lax。为什么不是Strict?因为Strict太严格了,用户从第三方链接点进来,连登录状态都没有,体验很差。

小技巧:设置SameSite时,记得同时加上SecureHttpOnly。这三个属性一起用,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,自然伪造不了请求。

具体做法:

  1. 用户访问页面时,服务器生成一个随机Token
  2. Token存在Session里,同时渲染到表单的隐藏域或请求头里
  3. 用户提交请求时,带上Token
  4. 服务器比对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一定要用密码学安全的随机数生成器。别用Math.random(),那玩意儿不够随机。我曾经见过有人用时间戳当Token,结果被攻击者暴力破解了。

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:这是浏览器的安全策略
我的建议:Referer检查适合作为辅助手段,不要单独依赖它。最好和Token验证或SameSite配合使用。

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

终极建议:如果你用的是现代框架(React、Vue、Angular),它们自带的CSRF防护机制已经够用了。别自己造轮子,除非你真的很清楚自己在做什么。

最后总结一下:CSRF防御的核心思路就两个——让攻击者拿不到凭证(Token)、让浏览器不自动带凭证(SameSite)。把这两个想明白了,CSRF就防住了。