CSRF(Cross-Site Request Forgery,跨站请求伪造)漏洞的本质是攻击者诱导已登录用户浏览器向目标网站发起一个非用户本意的请求,而这个请求因为自动携带了目标域名下的Cookie,导致服务端误以为是用户本人操作。修复CSRF不能只靠加一个Token了事,很多开发团队以为加了Token就安全,实际上Token的生成、校验位置、存储方式以及接口的业务属性都会影响防护效果。下面从攻击路径还原开始,直接给出可落地的修复方案和代码示例。
先确认哪些接口真正需要CSRF防护并不是所有接口都需要CSRF Token。按照HTTP方法划分,GET请求不应产生状态变更,如果系统里存在GET方式修改数据、删除资源、发布内容的接口,首先要改成POST/PUT/DELETE,而不是急着加Token。对于纯查询类GET接口,CSRF攻击者无法读取响应内容,只能触发请求,因此一般不需要防护。需要防护的是所有会改变服务端状态或触发敏感操作的POST、PUT、DELETE、PATCH接口,例如修改密码、转账、修改邮箱、删除文章、发布评论等。判断标准很简单:这个接口如果被攻击者用img标签或form自动提交触发,会不会造成实际损失。会,就必须防护。
CSRF Token的正确生成与校验逻辑Token必须满足随机性、一次性、绑定会话三个条件。随机性要求使用密码学安全的随机数生成器,不能使用时间戳或简单的Math.random。一次性要求每个表单或每个会话周期内Token唯一,并且服务端校验后立即失效或轮换。绑定会话要求Token与当前登录用户的SessionID强关联,不能做成全局通用的固定值。推荐做法是用户登录成功后,在服务端Session中生成一个CSRF Token,同时下发到前端页面。前端提交请求时通过请求头或表单字段带回,服务端取出Session中的Token进行恒等比较。
// Java 生成CSRF Token示例(使用SecureRandom)
import java.security.SecureRandom;
import java.util.Base64;
public class CsrfTokenUtil {
private static final SecureRandom secureRandom = new SecureRandom();
public static String generateToken() {
byte[] randomBytes = new byte[32];
secureRandom.nextBytes(randomBytes);
return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes);
}
}
// 登录成功后存入Session
HttpSession session = request.getSession(true);
String csrfToken = CsrfTokenUtil.generateToken();
session.setAttribute("csrf_token", csrfToken);
校验时不能只做简单的字符串对比。如果使用普通字符串equals,存在时序攻击风险,攻击者可以通过响应时间差异逐字节猜测Token内容。应使用恒定时间比较函数,例如Java中的MessageDigest.isEqual,或PHP中的hash_equals。另外,Token校验失败时应直接返回403状态码并终止请求处理,不能继续执行业务逻辑。
// 服务端校验CSRF Token(Java)
String sessionToken = (String) session.getAttribute("csrf_token");
String requestToken = request.getHeader("X-CSRF-Token");
if (sessionToken == null || requestToken == null) {
response.setStatus(403);
return;
}
// 使用恒定时间比较防止时序攻击
if (!MessageDigest.isEqual(
sessionToken.getBytes(StandardCharsets.UTF_8),
requestToken.getBytes(StandardCharsets.UTF_8))) {
response.setStatus(403);
return;
}
// 校验通过后继续业务处理
Token放在哪里更安全
CSRF Token的传递方式有三种:表单隐藏字段、自定义请求头、URL查询参数。URL查询参数是最不推荐的方式,因为URL会被记录在浏览器历史、服务器日志、Referer头中,容易泄漏。表单隐藏字段适用于传统表单提交场景,但要注意如果页面存在XSS漏洞,攻击者可以直接读取表单中的Token,因此CSRF防护的前提是XSS已经得到有效控制。自定义请求头是目前最安全的方式,因为浏览器同源策略禁止跨域请求携带自定义请求头,攻击者无法通过form或img标签触发带自定义头的请求。对于前后端分离架构,建议统一使用X-CSRF-Token请求头传递Token。
双重提交Cookie方案的适用场景与限制如果系统架构无法在服务端Session中存储Token,例如无状态API服务或分布式环境下Session共享成本过高,可以采用双重提交Cookie方案。具体做法是服务端生成一个随机Token,通过Set-Cookie写入用户浏览器,同时前端JavaScript读取该Cookie值,并在后续请求中通过X-CSRF-Token请求头回传。服务端校验请求头中的Token与Cookie中的Token是否一致。这个方案不依赖服务端存储,但有一个关键前提:攻击者无法读取或写入目标域下的Cookie。如果目标站点存在子域Cookie注入漏洞,或者攻击者能够控制某个子域,双重提交Cookie方案就会被绕过。因此该方案只适用于Cookie安全属性配置完善、无子域风险、且XSS防护到位的场景。
SameSite Cookie属性的实际防护效果SameSite属性是浏览器层面的CSRF防线。将Session Cookie设置为SameSite=Lax,可以阻止大多数跨站POST请求自动携带Cookie。Lax模式下,只有顶级导航的GET请求会携带Cookie,跨站form POST、跨站AJAX、iframe中的请求都不会携带。SameSite=Strict则更严格,任何跨站请求都不携带Cookie,但会影响用户从外部链接点击进入网站时的登录状态。对于大多数业务系统,SameSite=Lax配合CSRF Token是性价比较高的组合。需要注意两点:一是SameSite属性需要浏览器支持,老旧浏览器可能忽略该属性;二是如果业务中存在合法的跨站子域调用,需要单独配置SameSite=None; Secure,但这会削弱CSRF防护,必须配合Token使用。
登录接口和敏感操作的额外防护登录接口本身也存在CSRF风险,攻击者可以诱导用户登录到攻击者自己的账号,然后通过后续操作窃取用户在目标网站上的输入内容。这种攻击称为登录CSRF。防护方法是在登录页面生成一个预登录Token,用户提交登录表单时必须携带该Token,服务端验证通过后才建立会话。对于修改密码、修改绑定邮箱、转账等高风险操作,除了CSRF Token外,还应增加二次身份验证,例如要求输入当前密码、发送短信验证码或邮箱验证码。这样即使CSRF Token被绕过,攻击者也无法完成敏感操作。
Token过期与多标签页问题很多系统使用Session存储CSRF Token,但用户可能同时打开多个标签页操作。如果Token在每次请求后轮换,会导致其他标签页中的旧Token失效,用户提交时被误拦截。解决方案有两种:一是Token在整个会话周期内保持不变,只在登录时生成一次,这样多标签页不会互相影响,但缺点是Token一旦泄漏,整个会话期间的CSRF防护都失效。二是采用Token池机制,服务端为每个会话维护一个有效的Token集合,每次请求校验Token是否在集合中,校验通过后可以保留该Token继续使用,同时定期清理过期Token。对于大多数中小型系统,会话级固定Token配合HTTPS和严格的Cookie属性已经足够安全。
框架内置CSRF防护的正确配置主流Web框架都提供了CSRF防护组件,但默认配置不一定适合所有场景。以Spring Security为例,默认开启CSRF防护后,所有POST请求都需要携带_csrf参数或X-CSRF-Token请求头。但很多开发者在集成时直接禁用了CSRF,或者在前后端分离项目中没有正确配置Token的获取方式。正确的做法是保留框架的CSRF防护,通过一个公开接口将Token下发给前端,前端在每次请求时自动附加。对于Django、Laravel、Express等框架,同样需要检查CSRF中间件是否覆盖了所有状态变更接口,以及Token的获取接口是否被错误地排除在防护之外。
修复后的验证方法修复完成后不能只靠代码审查确认,必须进行实际攻击模拟。构造一个外部页面,包含一个自动提交的form,目标地址为需要防护的接口,form中不携带CSRF Token。在已登录目标网站的浏览器中打开该外部页面,观察请求是否被服务端拒绝。同时测试自定义请求头方式,确认跨域请求无法携带自定义头。对于使用SameSite Cookie的场景,使用浏览器开发者工具查看跨站请求的Cookie是否被正确拦截。最后检查所有状态变更接口的HTTP方法是否符合规范,确保没有GET请求执行写操作。
CSRF修复的核心不是堆砌技术手段,而是准确识别所有状态变更入口,并确保每一个入口都有一致的校验逻辑。Token、SameSite、二次验证三者结合,可以覆盖绝大多数攻击场景。修复完成后定期进行安全回归测试,防止新功能上线时遗漏CSRF防护。
