4、CSRF攻击与防御:SameSite Cookie、Token验证、Referer校验

4.1 什么是CSRF?我踩过的坑

CSRF,全称叫跨站请求伪造。说白了就是:攻击者利用你的登录状态,在你不知情的情况下,替你去干坏事。

我记得刚工作那会儿,有个同事负责的论坛系统出了个问题。用户A在论坛发帖,莫名其妙就变成了管理员,还能删帖封号。查了半天,原来是CSRF攻击。攻击者在另一个网站上放了个隐藏表单,自动提交了一个「修改用户权限」的请求。因为用户A的登录Cookie还在有效期内,服务器就傻乎乎地信了。

嗯,这就是CSRF最典型的场景。你想想看,你登录了银行网站,然后不小心点开了某个钓鱼页面。那个页面悄悄发起一个转账请求,你的Cookie自动带上去了,银行服务器一看「哦,是本人操作」,钱就转走了。

⚠️ 核心问题: 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
  }
}))
📌 小提示: 2021年之后,Chrome默认把没有设置SameSite的Cookie当作Lax处理。所以如果你什么都没配,其实已经在用Lax了。但别依赖这个,显式配置更靠谱。

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>
⚠️ 避坑指南: 我曾经犯过一个错——把Token放在URL的query参数里。结果Referer泄露了Token,攻击者通过Referer拿到了Token。记住:Token一定要放在请求体里,别放URL上。

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) => {
  // 转账逻辑
});
📌 小提示: 别用Referer校验来保护GET请求。因为GET请求的Referer很容易被搜索引擎、社交分享等场景改变。只对POST、PUT、DELETE等写操作做校验。

4.5 三种方案怎么选?我的建议

说实话,没有银弹。我个人的做法是分层防御:

  1. 第一层:SameSite Cookie —— 浏览器层面兜底,配置简单,覆盖大部分场景。
  2. 第二层:Token验证 —— 核心防御,对关键接口(转账、改密、发帖)强制校验。
  3. 第三层:Referer校验 —— 辅助手段,作为日志记录和告警依据。

你想想看,三层都上了,攻击者得同时绕过三个关卡,难度就大多了。

💡 实战总结:
  • 新项目:直接用SameSite Lax + Token验证,够用了
  • 老项目改造:先加SameSite,再逐步加Token
  • 高安全场景(金融、政务):三层全上,再加验证码

嗯,这就是我对CSRF防御的理解。说白了,就是让攻击者「借不了你的身份,发不了你的请求」。记住这个核心,你就知道该怎么防了。