CSRF(跨站请求伪造):利用 Cookie 的 SameSite 属性与 Token 防御

CSRF(跨站请求伪造)防御实战:深入理解 SameSite 属性与 Token 机制

大家好,欢迎来到今天的网络安全技术讲座。今天我们聚焦一个在 Web 安全领域中极其重要但又常被忽视的问题——跨站请求伪造(Cross-Site Request Forgery, CSRF)

如果你是一名开发者、运维人员或安全工程师,那么你一定听说过 CSRF。它不像 SQL 注入那样直接导致数据泄露,也不像 XSS 那样能执行恶意脚本,但它却能在用户不知情的情况下,完成“看起来合法”的操作,比如转账、修改密码、删除账户等。一旦攻击成功,后果可能非常严重。


一、什么是 CSRF?为什么它危险?

定义

CSRF 是一种利用用户已登录的身份,在未经其同意的情况下,向目标网站发送恶意请求的攻击方式。

举个例子:

  • 用户登录了银行网站 A(如 https://bank.example.com),浏览器保存了 Cookie(如 sessionid=abc123)。
  • 用户访问了一个恶意网站 B(如 https://malicious-site.com)。
  • 恶意网站 B 的 HTML 或 JavaScript 发起一个 GET/POST 请求到银行网站 A,例如:
<img src="https://bank.example.com/transfer?to=attacker&amount=1000" />

这个请求会自动带上用户的 Cookie(因为同域策略允许),银行服务器认为这是合法请求,于是执行转账操作!

⚠️ 关键点:攻击者不需要知道用户的密码、Cookie 内容或其他敏感信息,只需要诱导用户访问恶意页面即可。


二、传统防御手段及其局限性

早期常见的防御方法包括:

方法 描述 缺点
Referer 检查 检查 HTTP 头中的 Referer 是否来自可信域名 可被伪造,部分浏览器禁用 Referer(如隐私模式)
Captcha 强制用户输入验证码 影响用户体验,无法阻止自动化工具
登录二次确认 所有敏感操作都需要重新验证身份 增加交互复杂度,易被绕过

这些方案要么不彻底,要么牺牲可用性。因此,现代 Web 应用更推荐使用两种成熟且高效的防御机制:SameSite Cookie 属性CSRF Token(令牌)


三、SameSite Cookie 属性:从源头切断攻击路径

什么是 SameSite?

SameSite 是一种新的 Cookie 属性,由 RFC 6265 补充定义,用于限制 Cookie 在跨站请求中是否发送。

它的值可以是以下三种之一:

SameSite 值 含义 行为描述
Strict 严格模式 Cookie 仅在同站点请求时发送(即请求 URL 和 Cookie 所属域名完全一致)
Lax 松散模式 主要允许 GET 请求(如链接点击)携带 Cookie,其他 POST 请求不会带
None 无限制 允许跨站发送,但必须同时设置 Secure(HTTPS)

⚠️ 注意:若未显式指定 SameSite,默认行为是 Lax(Chrome 84+ 开始生效)

实战代码示例(Node.js + Express)

假设我们有一个简单的登录接口:

app.post('/login', (req, res) => {
  // 假设这里验证了用户名和密码
  const user = authenticateUser(req.body.username, req.body.password);

  if (user) {
    res.cookie('sessionid', user.sessionId, {
      httpOnly: true,
      secure: true,        // 必须 HTTPS 才能传输
      sameSite: 'Lax',     // 推荐使用 Lax,平衡安全性与兼容性
      maxAge: 3600 * 1000   // 1小时过期
    });

    res.json({ success: true });
  } else {
    res.status(401).json({ error: 'Invalid credentials' });
  }
});

此时,如果用户访问恶意网站 B,尝试发起对银行网站 A 的请求:

<!-- 恶意网站 B -->
<img src="https://bank.example.com/transfer?to=attacker&amount=1000" />

由于 sameSite=Lax,该请求不会携带 Cookie!攻击失败。

✅ 优点:

  • 简单有效,无需前端额外逻辑;
  • 对大多数主流浏览器支持良好(Chrome、Firefox、Safari 均支持);

❌ 局限:

  • 如果你的应用需要跨站 API 调用(如第三方嵌入 iframe),则需设为 None 并配合 Secure
  • 不适用于所有场景(比如某些 OAuth 流程依赖跨站重定向);

四、CSRF Token:更细粒度的防护机制

SameSite 虽然强大,但在某些情况下仍不够。比如:

  • 某些旧浏览器不支持 SameSite;
  • 你需要允许跨站 AJAX 请求(如微前端架构);
  • 你想对每个请求做精确控制(如不同页面有不同的 token);

这时就需要引入 CSRF Token

核心思想

在每次表单提交或 AJAX 请求中加入一个随机生成的 token,服务器端校验该 token 是否匹配。

实现步骤如下:

  1. 服务端生成 token 并存储
  2. 前端渲染时注入 token(HTML 或 JS)
  3. 每次请求附带 token(通常放在 Header 或 Form Data)
  4. 后端验证 token 是否正确

示例代码(Express + EJS 模板引擎)

Step 1:生成并存储 token(可存入 session 或 Redis)

// middleware/csrfToken.js
const crypto = require('crypto');

function generateCsrfToken() {
  return crypto.randomBytes(32).toString('hex');
}

// 存储 token 到 session(实际项目建议用 Redis)
app.use((req, res, next) => {
  if (!req.session.csrfToken) {
    req.session.csrfToken = generateCsrfToken();
  }
  res.locals.csrfToken = req.session.csrfToken;
  next();
});

Step 2:模板中注入 token(EJS)

<!-- views/index.ejs -->
<form action="/transfer" method="post">
  <input type="hidden" name="_csrf" value="<%= csrfToken %>" />
  <label>To:</label>
  <input type="text" name="to" />
  <label>Amount:</label>
  <input type="number" name="amount" />
  <button type="submit">Transfer</button>
</form>

Step 3:后端校验 token(中间件)

// middleware/csrfCheck.js
function csrfCheck(req, res, next) {
  const clientToken = req.body._csrf || req.headers['x-csrf-token'];
  const serverToken = req.session.csrfToken;

  if (!clientToken || clientToken !== serverToken) {
    return res.status(403).json({ error: 'CSRF token mismatch' });
  }

  // 校验通过,清除 token(防止重放攻击)
  req.session.csrfToken = null;

  next();
}

// 应用中间件
app.use('/transfer', csrfCheck);

这样即使攻击者诱导用户点击恶意链接,也无法获取正确的 _csrf 字段,从而无法伪造请求。

✅ 优点:

  • 精确控制每一个请求;
  • 支持跨站 AJAX(只要客户端主动传 token);
  • 易于集成到现有框架(React/Vue/Angular 均可轻松实现);

❌ 缺点:

  • 需要前后端配合开发;
  • 若忘记校验,仍有风险;
  • Token 管理复杂(需防重放、定期刷新);

五、组合使用 SameSite + Token:最佳实践

理想情况下,我们应该将两者结合使用:

场景 SameSite 设置 Token 使用 说明
普通网页表单提交 Lax ✅ 必须 SameSite 阻止大部分攻击,Token 提供额外保障
AJAX 请求(跨站) None + Secure ✅ 必须 SameSite 不再适用,必须靠 Token 校验
微前端或 iframe 嵌套 None + Secure ✅ 必须 同上,token 是唯一防线
移动 App 内嵌 WebView StrictLax ❗ 可选 若 App 自带 Cookie 管理,可简化处理

推荐配置(Express 示例)

// 设置全局 Cookie 安全策略
app.use((req, res, next) => {
  res.cookie('sessionid', req.session.id, {
    httpOnly: true,
    secure: true,
    sameSite: 'Lax',
    maxAge: 3600 * 1000
  });

  // 对敏感路由启用 CSRF Token 校验
  if (req.path.startsWith('/api/') || req.path === '/transfer') {
    csrfCheck(req, res, next);
  } else {
    next();
  }
});

这种组合既能享受 SameSite 的便捷,又能利用 Token 的灵活性,真正构建“纵深防御”。


六、常见误区与注意事项

误区 正确做法
“我用了 SameSite 就不用 Token 了” SameSite 不能覆盖所有场景(如跨站 AJAX),Token 是必要补充
“我把 token 放在 localStorage 就安全了” localStorage 可被 XSS 攻击窃取,应优先使用内存变量或隐藏字段
“Token 一次就失效,永远不重复” 应设计为一次性 token,但允许短期缓存(如 5 分钟内有效)
“只检查 Referer 就够了” Referer 可伪造,不可作为主要防护手段

此外,还需注意以下几点:

  • 避免暴露 token 给第三方脚本(如 CDN 加载的外部 JS);
  • 定时刷新 token(尤其是长时间停留页面);
  • 日志记录异常请求(便于事后追踪);
  • 测试工具辅助检测(如 Burp Suite、OWASP ZAP);

七、总结与建议

CSRF 是一种隐蔽性强、危害大的攻击方式,但我们可以通过合理的防御策略将其扼杀在摇篮中。

✅ 最佳实践总结:

技术 推荐用途 是否必备
SameSite Cookie 属性 默认防护,尤其适合表单提交 ✅ 强烈推荐
CSRF Token 敏感操作、AJAX、跨站请求 ✅ 必备
Secure + HttpOnly 所有 Cookie 设置 ✅ 必须
Token 清除机制 防止重放攻击 ✅ 必须

记住一句话:“不要依赖单一防御,要用多层防护构建信任链。”

最后提醒一句:无论多么复杂的系统,安全的第一道防线永远是你自己的意识——保持更新知识、关注漏洞公告、定期审计代码,才是真正的高手之道。

感谢大家的聆听!希望今天的分享对你今后的设计与开发有所帮助。如有疑问,欢迎留言讨论!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注