跨站请求伪造(Cross-Site Request Forgery, CSRF)是一种常见且具有高度危险性的网络攻击方式。攻击者通过诱导已登录的合法用户在不知情的情况下,点击恶意链接或访问恶意页面,从而以用户的名义向目标网站发送伪造的请求,执行非预期的操作,如修改密码、转账、删除数据等。由于这些请求携带了用户的合法身份凭证(例如 Cookie),服务器会误认为是用户的真实意图,从而导致严重的安全后果。
本报告旨在深入探讨防范 CSRF 攻击的各项技术和策略。我们将重点分析目前业界公认最有效、应用最广泛的基于令牌(Token)的防御机制,并详细阐述其在不同架构(如有状态服务、无状态 API、单页应用)中的实现细节。此外,报告还将分析如何结合现代浏览器的安全新特性,如 SameSite Cookie 属性及其他 HTTP 安全头部,构建一个多层次、纵深化的防御体系,以应对日益复杂的网络安全挑战。
2. 核心防御基石:同步器令牌模式 (Synchronizer Token Pattern)
在所有 CSRF 防御技术中,同步器令牌模式(Synchronizer Token Pattern, STP)被公认为最有效、最成熟且应用最广泛的核心策略 。几乎所有现代 Web 框架都内置了基于此模式的防护功能 。
2.1 工作原理与机制
同步器令牌模式的核心思想是,在所有执行状态变更的请求中,增加一个攻击者无法预测的随机令牌。其工作流程如下:
- 令牌生成:当用户首次访问网站或登录成功后,服务器会为该用户的会话生成一个唯一且密码学安全的伪随机令牌(即 CSRF 令牌) 。这个令牌与用户的会话紧密关联,通常存储在服务器端的会话(Session)中 。
- 令牌传递:服务器将此令牌发送给客户端。在传统的 Web 应用中,令牌通常被嵌入到 HTML 表单的一个隐藏字段中 。对于现代单页应用(SPA),令牌可能通过 API 响应返回,或写入一个特定的 Cookie 中供 JavaScript 读取。
- 令牌提交:当用户提交表单或发起一个敏感操作(如 POST, PUT, DELETE 请求)时,浏览器会将这个 CSRF 令牌随请求一同发送给服务器 。令牌可以作为表单数据、URL 参数或自定义 HTTP 请求头(如
X-CSRF-TOKEN)的一部分提交。 - 令牌验证:服务器在接收到请求后,会从请求中提取出 CSRF 令牌,并与存储在用户会话中的令牌进行比较。如果两者匹配,则证明该请求是合法的,源自受信任的客户端,服务器会继续处理该请求。如果令牌缺失、不匹配或无效,服务器将拒绝该请求,因为它极有可能是 CSRF 攻击 。
2.2 有效性分析
该模式的有效性基于 CSRF 攻击的一个关键前提:攻击者可以伪造请求的全部内容,但无法读取或获取目标网站域下的响应内容或受保护的资源(如同源策略所限制)。因此,攻击者无法得知服务器为用户会话生成的那个秘密令牌,也就无法构建一个包含正确令牌的伪造请求 。这使得伪造的请求在服务器端验证时会立刻失败。
3. 主流 Web 框架中的内置 CSRF 防护实践
为了简化开发并提高安全性,绝大多数主流 Web 开发框架都提供了开箱即用或易于配置的 CSRF 防护机制。开发者应优先使用并正确配置这些内置功能,而非自行实现 。
-
Django (Python) :Django 的 CSRF 保护是默认启用的,通过一个名为
CsrfViewMiddleware的中间件实现 。在模板中,开发者只需在表单内使用{% csrf_token %}标签,Django 就会自动生成并插入一个包含 CSRF 令牌的隐藏输入字段 。对于 AJAX 请求,JavaScript 需要从csrftokenCookie 中读取令牌,并将其设置在X-CSRFToken请求头中 。 -
Laravel (PHP) :Laravel 同样内置了强大的 CSRF 防护,通过
VerifyCsrfToken中间件自动对所有非GET、HEAD、OPTIONS的请求进行验证 。在 Blade 模板中,使用@csrf指令即可轻松生成令牌字段 。Laravel 还会自动将令牌存储在XSRF-TOKENCookie 中,一些 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 防护的状态完全转移到客户端 :
- 令牌生成与存储:用户认证成功后,认证服务器(或网关)生成一个 CSRF 令牌,并将其设置为一个客户端 Cookie。这个 Cookie 不应设置
HttpOnly标志,以便客户端 JavaScript 可以读取它 。 - 令牌提交:客户端的 JavaScript 代码在每次发起敏感请求时,需要从 Cookie 中读取这个令牌,并将其值复制到另一个地方,通常是自定义的 HTTP 请求头(如
X-CSRF-TOKEN)中 。 - 令牌验证:无状态的后端服务在收到请求后,只需比较请求头中的令牌值和 Cookie 中的令牌值是否完全一致。如果一致,则验证通过 。
4.2 有效性与安全考量
这种方法的安全性依赖于浏览器的同源策略(Same-Origin Policy),它禁止恶意网站的脚本读取或修改其他域下的 Cookie 。攻击者可以伪造一个请求,让用户的浏览器自动携带目标域的 Cookie,但攻击者的脚本无法读取这个 Cookie 的内容,因此也无法将正确的值复制到请求头中。
为了增强安全性,存储令牌的 Cookie 应使用 Secure 标志确保只在 HTTPS 下传输,并可结合 SameSite 属性提供额外保护 。
5. 现代单页应用 (SPA) 中的令牌管理高级实践
单页应用(SPA)完全依赖 API 与后端通信,因此需要一套健壮的客户端 CSRF 令牌管理流程。这不仅包括获取和提交令牌,还应考虑令牌的轮换(Rotation)以进一步提升安全性。
5.1 令牌获取与自动提交
最常见的实践是使用 HTTP 库的拦截器(Interceptors)来自动化此过程。以 Axios 为例:
-
初始令牌获取:SPA 在初始化时,可以向后端的一个专用 API 端点(如
https://files.metaso.cn/api/csrf-token)发送请求,获取初始的 CSRF 令牌 。后端也可以在用户登录成功后的响应中直接返回令牌。 -
配置拦截器:配置 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 攻击的最佳实践是一个多层次的综合策略。其核心仍然是同步器令牌模式,它通过要求每个状态变更请求都携带一个不可预测的秘密令牌,提供了最直接和最可靠的保护。
我们的研究得出以下关键结论:
- 令牌是核心:无论是传统的同步器令牌模式,还是适用于无状态架构的双提交 Cookie 模式,基于令牌的验证机制是抵御 CSRF 攻击的基石。
- 拥抱框架:开发者应充分利用现代 Web 框架(如 Django, Laravel, Spring Security)提供的内置 CSRF 防护功能,它们经过了广泛测试且易于实施。
- 适配架构:针对单页应用(SPA)和微服务,必须采用与之相适应的令牌管理策略,例如使用拦截器自动化令牌提交和实现令牌轮换。
- 构建纵深防御:绝不能依赖单一的防御措施。必须将 CSRF 令牌与现代浏览器的安全特性相结合,特别是将会话 Cookie 的
SameSite属性设置为Lax或Strict,并部署严格的 CSP 和其他安全 HTTP 头部,从而构建一个难以被攻破的纵深防御体系。
通过遵循这些原则,开发团队可以有效地保护他们的 Web 应用程序免受 CSRF 攻击的威胁,确保用户数据的安全和应用的完整性。
6237

被折叠的 条评论
为什么被折叠?



