如何防范 CSRF 攻击?(例如使用 Token)

跨站请求伪造(Cross-Site Request Forgery, CSRF)是一种常见且具有高度危险性的网络攻击方式。攻击者通过诱导已登录的合法用户在不知情的情况下,点击恶意链接或访问恶意页面,从而以用户的名义向目标网站发送伪造的请求,执行非预期的操作,如修改密码、转账、删除数据等。由于这些请求携带了用户的合法身份凭证(例如 Cookie),服务器会误认为是用户的真实意图,从而导致严重的安全后果。

本报告旨在深入探讨防范 CSRF 攻击的各项技术和策略。我们将重点分析目前业界公认最有效、应用最广泛的基于令牌(Token)的防御机制,并详细阐述其在不同架构(如有状态服务、无状态 API、单页应用)中的实现细节。此外,报告还将分析如何结合现代浏览器的安全新特性,如 SameSite Cookie 属性及其他 HTTP 安全头部,构建一个多层次、纵深化的防御体系,以应对日益复杂的网络安全挑战。

2. 核心防御基石:同步器令牌模式 (Synchronizer Token Pattern)

在所有 CSRF 防御技术中,同步器令牌模式(Synchronizer Token Pattern, STP)被公认为最有效、最成熟且应用最广泛的核心策略 。几乎所有现代 Web 框架都内置了基于此模式的防护功能 。

2.1 工作原理与机制

同步器令牌模式的核心思想是,在所有执行状态变更的请求中,增加一个攻击者无法预测的随机令牌。其工作流程如下:

  1. 令牌生成:当用户首次访问网站或登录成功后,服务器会为该用户的会话生成一个唯一且密码学安全的伪随机令牌(即 CSRF 令牌) 。这个令牌与用户的会话紧密关联,通常存储在服务器端的会话(Session)中 。
  2. 令牌传递:服务器将此令牌发送给客户端。在传统的 Web 应用中,令牌通常被嵌入到 HTML 表单的一个隐藏字段中 。对于现代单页应用(SPA),令牌可能通过 API 响应返回,或写入一个特定的 Cookie 中供 JavaScript 读取。
  3. 令牌提交:当用户提交表单或发起一个敏感操作(如 POST, PUT, DELETE 请求)时,浏览器会将这个 CSRF 令牌随请求一同发送给服务器 。令牌可以作为表单数据、URL 参数或自定义 HTTP 请求头(如 X-CSRF-TOKEN)的一部分提交。
  4. 令牌验证:服务器在接收到请求后,会从请求中提取出 CSRF 令牌,并与存储在用户会话中的令牌进行比较。如果两者匹配,则证明该请求是合法的,源自受信任的客户端,服务器会继续处理该请求。如果令牌缺失、不匹配或无效,服务器将拒绝该请求,因为它极有可能是 CSRF 攻击 。
2.2 有效性分析

该模式的有效性基于 CSRF 攻击的一个关键前提:攻击者可以伪造请求的全部内容,但无法读取或获取目标网站域下的响应内容或受保护的资源(如同源策略所限制)。因此,攻击者无法得知服务器为用户会话生成的那个秘密令牌,也就无法构建一个包含正确令牌的伪造请求 。这使得伪造的请求在服务器端验证时会立刻失败。

3. 主流 Web 框架中的内置 CSRF 防护实践

为了简化开发并提高安全性,绝大多数主流 Web 开发框架都提供了开箱即用或易于配置的 CSRF 防护机制。开发者应优先使用并正确配置这些内置功能,而非自行实现 。

  • Django (Python) :Django 的 CSRF 保护是默认启用的,通过一个名为 CsrfViewMiddleware 的中间件实现 。在模板中,开发者只需在表单内使用 {% csrf_token %} 标签,Django 就会自动生成并插入一个包含 CSRF 令牌的隐藏输入字段 。对于 AJAX 请求,JavaScript 需要从 csrftoken Cookie 中读取令牌,并将其设置在 X-CSRFToken 请求头中 。

  • Laravel (PHP) :Laravel 同样内置了强大的 CSRF 防护,通过 VerifyCsrfToken 中间件自动对所有非 GETHEADOPTIONS 的请求进行验证 。在 Blade 模板中,使用 @csrf 指令即可轻松生成令牌字段 。Laravel 还会自动将令牌存储在 XSRF-TOKEN Cookie 中,一些 JavaScript 库(如 Axios)可以自动读取并用于 AJAX 请求 。

  • Spring Security (Java) :Spring Security 默认启用 CSRF 保护 。默认情况下,它使用 HttpSessionCsrfTokenRepository 将令牌存储在服务器的 HttpSession 中 。对于前后端分离的 SPA 应用,更推荐配置 CookieCsrfTokenRepository,它会将令牌存储在客户端的 Cookie 中,便于 JavaScript 访问 。开发者可以通过简单的 Java 配置来启用和定制此功能 :

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            );
        // ... 其他配置
    }
}
  • Node.js (Express) :Express 框架本身不内置 CSRF 防护,但可以通过成熟的第三方中间件(如 csurf)轻松集成 。csurf 中间件与会话管理库(如 express-session)配合使用,生成和验证令牌 。

4. 无状态架构与微服务中的 CSRF 防护

在微服务和无状态 API 架构中,服务器不维护用户会话,这使得传统的基于 Session 的同步器令牌模式不再适用。在这种场景下,“双提交 Cookie”(Double Submit Cookie)模式成为了一种优秀的替代方案 。

4.1 双提交 Cookie 模式原理

该模式的核心思想是将 CSRF 防护的状态完全转移到客户端 :

  1. 令牌生成与存储:用户认证成功后,认证服务器(或网关)生成一个 CSRF 令牌,并将其设置为一个客户端 Cookie。这个 Cookie 不应设置 HttpOnly 标志,以便客户端 JavaScript 可以读取它 。
  2. 令牌提交:客户端的 JavaScript 代码在每次发起敏感请求时,需要从 Cookie 中读取这个令牌,并将其值复制到另一个地方,通常是自定义的 HTTP 请求头(如 X-CSRF-TOKEN)中 。
  3. 令牌验证:无状态的后端服务在收到请求后,只需比较请求头中的令牌值和 Cookie 中的令牌值是否完全一致。如果一致,则验证通过 。
4.2 有效性与安全考量

这种方法的安全性依赖于浏览器的同源策略(Same-Origin Policy),它禁止恶意网站的脚本读取或修改其他域下的 Cookie 。攻击者可以伪造一个请求,让用户的浏览器自动携带目标域的 Cookie,但攻击者的脚本无法读取这个 Cookie 的内容,因此也无法将正确的值复制到请求头中。

为了增强安全性,存储令牌的 Cookie 应使用 Secure 标志确保只在 HTTPS 下传输,并可结合 SameSite 属性提供额外保护 。

5. 现代单页应用 (SPA) 中的令牌管理高级实践

单页应用(SPA)完全依赖 API 与后端通信,因此需要一套健壮的客户端 CSRF 令牌管理流程。这不仅包括获取和提交令牌,还应考虑令牌的轮换(Rotation)以进一步提升安全性。

5.1 令牌获取与自动提交

最常见的实践是使用 HTTP 库的拦截器(Interceptors)来自动化此过程。以 Axios 为例:

  1. 初始令牌获取:SPA 在初始化时,可以向后端的一个专用 API 端点(如 https://files.metaso.cn/api/csrf-token)发送请求,获取初始的 CSRF 令牌 。后端也可以在用户登录成功后的响应中直接返回令牌。

  2. 配置拦截器:配置 Axios 的请求拦截器,使其在每个发出的请求中自动附带 CSRF 令牌。令牌可以从之前存储的地方(如 JavaScript 变量、localStorage 或 meta 标签)读取 。

// 假设 token 存储在 meta 标签中
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');

axios.defaults.headers.common['X-CSRF-TOKEN'] = csrfToken;

// 或者使用拦截器
axios.interceptors.request.use(config => {
    config.headers['X-CSRF-TOKEN'] = getCsrfTokenFromSomewhere(); // 从某处获取令牌
    return config;
});
5.2 令牌轮换机制

为了防止令牌泄露后被长期利用(例如,通过其他漏洞如 XSS),最佳实践是在每次成功执行状态变更操作后轮换 CSRF 令牌。

  • 后端 API 设计:后端的 API 端点(如处理 POST/PUT/DELETE 请求的控制器)在成功处理请求并验证旧令牌后,应生成一个新的 CSRF 令牌,并通过响应体或响应头返回给客户端 。例如,Laravel 控制器可以这样实现:

public function updateUser(Request $request) {
    // ... 更新用户信息 ...

    $request->session()->regenerateToken(); // 重新生成令牌
    $newToken = csrf_token();

    return response()->json([
        'message' => 'User updated successfully',
        'new_csrf_token' => $newToken
    ]);
}
  • 前端实现:前端在接收到成功的响应后,应检查响应中是否包含新的令牌。如果存在,就用新令牌更新本地存储的值,以供下一次请求使用 。这可以通过 Axios 的响应拦截器来实现,使其对所有成功响应进行统一处理。

6. 纵深防御:结合现代浏览器安全策略

虽然基于令牌的机制是防御 CSRF 的核心,但任何单一的防御措施都可能存在弱点。因此,构建一个多层次的纵深防御体系至关重要 。这涉及到结合利用现代浏览器提供的多种安全功能。

6.1 SameSite Cookie 属性

SameSite 是一个强大的浏览器原生 CSRF 防御机制,它通过限制 Cookie 在跨站请求中的发送来从源头上遏制攻击 。它有三个值:

  • Strict:最严格的模式。Cookie 完全禁止在任何跨站请求中发送,即使用户从其他网站点击链接跳转过来也不行。这提供了最强的 CSRF 保护,但可能会影响用户体验(例如,从外部链接点击进入后需要重新登录)。
  • Lax:默认值。允许在一些安全的顶层导航(如点击链接 <a href="...">)的 GET 请求中发送 Cookie,但在 POST 请求或通过 <iframe><img> 等加载资源的跨站请求中则会阻止发送。它在安全性和用户体验之间取得了很好的平衡 。
  • None:允许在所有跨站请求中发送 Cookie,但必须同时指定 Secure 属性(即只在 HTTPS 中发送)。适用于需要跨域嵌入或 API 调用的场景 。

策略:将所有用于身份验证的会话 Cookie 设置为 SameSite=Lax 或 SameSite=Strict,可以显著降低 CSRF 风险。但这不能完全替代 CSRF 令牌,因为它仍有浏览器兼容性问题,且 Lax 模式无法防御通过链接发起的 GET 请求型攻击 。

6.2 其他关键 HTTP 安全头

将 CSRF 令牌与以下 HTTP 安全头结合使用,可以构建更坚固的防线:

  • 验证 Origin 和 Referer 头:服务器可以检查请求的 Origin 或 Referer 头,确保请求来自预期的源站点。这是一个有用的辅助检查手段,但这些头部可能被用户或代理移除,或在某些情况下被伪造 。

  • 内容安全策略 (Content-Security-Policy, CSP) :CSP 主要用于防御跨站脚本(XSS)攻击。由于 XSS 漏洞可以被用来窃取 CSRF 令牌,从而绕过防护,因此部署严格的 CSP 策略是 CSRF 防御体系中不可或缺的一环 。

  • 跨源开启者策略 (Cross-Origin-Opener-Policy, COOP) :通过设置 COOP: same-origin,可以确保顶层文档与它打开的任何跨源文档隔离,防止它们之间直接的 DOM 访问,从而减少潜在的攻击面 。

6.3 综合配置示例 (Node.js/Express)

以下是一个在 Node.js/Express 应用中实现纵深防御的配置示例,它结合了 csurf 令牌保护和多项安全头:

const express = require('express');
const session = require('express-session');
const cookieParser = require('cookie-parser');
const csrf = require('csurf');
const helmet = require('helmet'); // 用于设置多种安全头

const app = express();

// ... 其他中间件配置 ...

app.use(cookieParser());
app.use(session({
    secret: 'your-secret-key',
    resave: false,
    saveUninitialized: true,
    cookie: { 
        secure: true, 
        httpOnly: true, 
        sameSite: 'lax' // 设置 SameSite 属性
    }
}));

// 使用 Helmet 设置安全头
app.use(helmet.contentSecurityPolicy({
    directives: {
        defaultSrc: ["'self'"],
        // ... 其他 CSP 规则
    }
}));
app.use(helmet.referrerPolicy({ policy: 'same-origin' }));
app.use((req, res, next) => {
    res.setHeader('Cross-Origin-Opener-Policy', 'same-origin'); // 设置 COOP [[258]]
    next();
});

// 启用 csurf 中间件
const csrfProtection = csrf({ cookie: true });
app.use(csrfProtection);

app.get('/form', (req, res) => {
    // 将 CSRF 令牌传递给视图
    res.render('send', { csrfToken: req.csrfToken() });
});

app.post('/process', (req, res) => {
    res.send('Data is being processed');
});

// ... 启动服务器 ...

7. 结论

截至2025年9月30日,防范 CSRF 攻击的最佳实践是一个多层次的综合策略。其核心仍然是同步器令牌模式,它通过要求每个状态变更请求都携带一个不可预测的秘密令牌,提供了最直接和最可靠的保护。

我们的研究得出以下关键结论:

  1. 令牌是核心:无论是传统的同步器令牌模式,还是适用于无状态架构的双提交 Cookie 模式,基于令牌的验证机制是抵御 CSRF 攻击的基石。
  2. 拥抱框架:开发者应充分利用现代 Web 框架(如 Django, Laravel, Spring Security)提供的内置 CSRF 防护功能,它们经过了广泛测试且易于实施。
  3. 适配架构:针对单页应用(SPA)和微服务,必须采用与之相适应的令牌管理策略,例如使用拦截器自动化令牌提交和实现令牌轮换。
  4. 构建纵深防御:绝不能依赖单一的防御措施。必须将 CSRF 令牌与现代浏览器的安全特性相结合,特别是将会话 Cookie 的 SameSite 属性设置为 Lax 或 Strict,并部署严格的 CSP 和其他安全 HTTP 头部,从而构建一个难以被攻破的纵深防御体系。

通过遵循这些原则,开发团队可以有效地保护他们的 Web 应用程序免受 CSRF 攻击的威胁,确保用户数据的安全和应用的完整性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

破碎的天堂鸟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值