Java Web安全审计实战:深入剖析CSRF漏洞原理、检测与防御
1. 项目概述:为什么CSRF是Java Web安全的“隐形杀手”?
做Java开发或者安全审计的朋友,对SQL注入、XSS这些漏洞肯定不陌生,但CSRF(跨站请求伪造)这个漏洞,很多时候就像个“隐形杀手”。它不像注入攻击那样直接操作数据库,也不像XSS那样直观地弹个窗,它的攻击过程往往在用户毫无察觉的情况下完成。你可能觉得,现在框架这么成熟,Spring Security用上,CSRF不就自动防住了吗?我做了十多年的代码审计,见过太多因为配置疏忽、逻辑遗漏或者对框架机制理解不透彻而导致的CSRF漏洞。尤其是在一些老系统、自研框架或者对安全要求极高的金融、交易类应用中,一个CSRF漏洞可能意味着用户账户被篡改、资金被转移,而攻击者甚至不需要知道你的密码。
简单来说,CSRF攻击就是“借刀杀人”。攻击者诱导已经登录了目标网站(比如你的银行网站)的用户,去访问一个恶意页面。这个页面里暗藏了一个向目标网站发起请求的脚本或表单。由于用户的浏览器会自动携带登录凭证(比如Session Cookie),目标网站会认为这是用户本人的合法操作,从而执行了攻击者预设的请求,比如修改密码、转账、发表评论等。整个过程,用户可能只是在浏览一个普通的帖子或者图片,攻击完全在后台静默完成。
所以,这次我们就深入Java代码层面,掰开揉碎地聊聊CSRF。不止是讲原理,更重要的是,我会结合大量真实的审计案例,带你看看CSRF漏洞在代码里到底长什么样,Spring Security的防御机制在什么情况下会“失灵”,以及我们该如何从代码审计的角度,系统性地发现和验证这类漏洞。无论你是开发想写出更安全的代码,还是安全工程师想提升审计效率,这篇文章都能给你直接的、可落地的参考。
2. CSRF漏洞核心原理与Java Web场景下的特殊性
要审计,必须先理解原理,而且得是结合了Java Web特性的原理。很多人对CSRF的理解停留在“伪造请求”上,这不够。我们需要知道在Java的Servlet/JSP、Spring MVC等生态下,请求是如何被处理、会话是如何维持的,漏洞点才会清晰。
2.1 攻击链条的三要素与Java实现
一次成功的CSRF攻击,离不开三个核心条件,在Java Web环境中,它们有具体的表现形式:
用户已登录并持有有效的会话凭证:这是基础。在Java中,这通常意味着用户的浏览器里存有一个名为
JSESSIONID的Cookie,这个Cookie对应服务器端(如Tomcat)的一个HttpSession对象。服务器通过这个ID来识别用户身份。只要这个会话没有过期,并且包含了认证信息(比如session.setAttribute(“user”, userObj)),后续请求就会被认为是该用户的。目标站点存在可预测或未受保护的状态变更操作:攻击者需要找到一个可以“利用”的端点。在Java里,这通常是一个Controller的
@RequestMapping方法,它处理诸如/user/updateEmail、/transfer、/admin/deleteUser等请求。关键点在于,这个端点缺乏对请求来源的验证。它只认JSESSIONID,不关心这个请求是从www.bank.com发来的,还是从evil.com的一个图片标签<img src=”http://www.bank.com/transfer?to=attacker&amount=10000”>发来的。用户被诱导访问恶意页面:攻击者需要让已登录的用户“触发”这个请求。在Java Web的语境下,攻击载荷(Payload)的构造非常灵活:
- GET型CSRF:利用
<img>、<script>、<iframe>等标签的src属性,或者<a>标签的href,直接发起一个GET请求。这种方式简单,但只适用于用GET方法进行状态变更的接口(这本身也是不安全的RESTful实践)。 - POST型CSRF:更常见。恶意页面构造一个隐藏的
<form>,自动提交到目标地址。或者使用JavaScript的fetch或XMLHttpRequest发起AJAX请求。虽然浏览器同源策略会阻止读取跨域响应,但请求本身会被发出并执行,这才是CSRF危险的关键。
- GET型CSRF:利用
这里有一个至关重要的细节:同源策略(SOP)限制的是跨域读取响应,而不是发送请求。恶意网站evil.com无法读取bank.com返回的转账成功页面内容,但浏览器向bank.com发送请求并携带Cookie这个动作是允许的。服务器处理了请求,攻击就生效了。
2.2 与XSS的本质区别:信任的边界
新手常混淆CSRF和XSS,但它们的信任模型截然不同,审计时的关注点也不同。
- XSS(跨站脚本):漏洞发生在目标网站本身。攻击者将恶意脚本注入到目标网站中(如评论区),当其他用户浏览该页面时,脚本在其浏览器中执行。此时,脚本运行在
bank.com的源(Origin)下,可以完全访问该源下的Cookie、LocalStorage等所有资源。XSS利用了用户对目标网站的信任。 - CSRF(跨站请求伪造):漏洞也发生在目标网站(缺乏来源验证),但攻击的触发点在另一个网站(
evil.com)。恶意脚本在evil.com下运行,它无法读取bank.com的Cookie,但它可以指挥浏览器向bank.com发送一个携带了Cookie的请求。CSRF利用了网站对用户浏览器的信任(即“浏览器会自动在请求中附加Cookie”这一机制)。
在代码审计时,XSS的寻找点是“未过滤的输出”,而CSRF的寻找点是“未验证来源的敏感操作”。
2.3 Java生态中常见的“安全错觉”
很多团队认为使用了主流框架就高枕无忧,这恰恰是危险的开始。以下是我在审计中经常遇到的几种“安全错觉”场景:
- Spring Security配置不完整:只配置了
http.formLogin()和权限规则,但没有显式启用CSRF防护(http.csrf().disable()被错误调用,或者压根没配置http.csrf())。在Spring Security 4.x之后,默认是启用CSRF防护的,但很多从旧版本迁移或参考了过时教程的项目,会手动关闭它。 - 错误地排除防护:知道要开启CSRF防护,但为了“方便”,使用
.csrf().ignoringAntMatchers(“/api/**”)把整个API接口排除了。理由是“API是无状态的,用Token认证”。问题在于,如果这个API接口同时被浏览器端调用(比如一个传统的表单提交到/api/update),它依然暴露在CSRF风险下。正确的做法是,对于需要从浏览器发起的API,依然需要CSRF Token;对于纯后端调用的API,使用如JWT等无状态认证,并确保不在浏览器环境中存储。 - 自定义Filter的顺序错误:自己实现了认证Filter,但把它放在了Spring Security的
CsrfFilter之前。导致请求先被你的Filter处理并可能创建了会话,然后才经过CSRF校验。如果逻辑不当,可能绕过检查。 - 对
@Controller和@RestController的误解:认为所有@RestController(返回JSON的接口)都不需要CSRF防护。这是一个误区。如果这个RestController的端点是通过浏览器表单提交或前端框架(如Thymeleaf, JSP)渲染的页面发起的,它同样需要防护。只有当客户端是移动App、桌面程序或其他服务,且使用如OAuth2、JWT等与Cookie会话无关的认证方式时,才可以考虑豁免。
3. 代码审计实战:定位CSRF漏洞的四大切入点
理解了原理,我们带上“审计眼镜”,开始扫描代码。我通常从以下几个关键切入点进行系统性地排查,效率最高。
3.1 切入点一:审查安全配置(web.xml, Spring Config)
这是第一道,也是最快的一道筛查。
1. 传统Servlet/JSP项目(web.xml): 检查是否有自定义的、用于校验Referer或Token的Filter,以及它的<filter-mapping>顺序。如果没有任何相关的Filter配置,那么CSRF防护基本依赖于应用自身的、在每个接口里的校验,风险很高。
2. Spring Boot / Spring MVC项目: 这是重灾区。直接打开你的安全配置类(通常继承WebSecurityConfigurerAdapter或使用新版的SecurityFilterChainBean)。
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/public/**").permitAll() .anyRequest().authenticated() .and() .formLogin() .and() // 关键点在这里:是否启用了csrf()? // 情况A:完全没写这一行 -> 在Spring Security 4+ 默认是启用的,但最好显式写明。 // 情况B:错误地禁用了它 -> 高危! .csrf().disable(); // <-- 这是危险信号! } }审计要点:
- 找到
.csrf()的配置。 - 如果调用了
.disable(),立即标记为高危。必须结合业务确认是否真的不需要。 - 如果调用了
.ignoringAntMatchers(...),仔细审查被排除的URL模式。排除登录、注销、公开API(需确认无状态)是合理的,排除/user/**,/order/**等敏感操作是危险的。
3.2 切入点二:扫描敏感操作接口(Controller层)
即使全局配置了CSRF防护,也可能有个别接口需要特殊处理或被意外绕过。我们需要人工+工具扫描所有状态变更接口。
1. 识别敏感操作: 在Controller中,关注以下注解和方法:
@PostMapping(或@RequestMapping(method = RequestMethod.POST)):这是CSRF的主要攻击目标。@PutMapping,@DeleteMapping,@PatchMapping:RESTful接口,同样危险。- 特别注意:
@GetMapping但执行了修改操作(如/delete?id=1)。这是不安全的RESTful实践,且极易受到GET型CSRF攻击。
2. 检查防护是否生效: 对于Spring MVC,如果启用了CSRF,视图层(如JSP, Thymeleaf)中使用<form:form>标签会自动添加一个名为_csrf的隐藏域。但有时开发会使用原生HTML表单:
<!-- 危险:原生表单,无CSRF Token --> <form action="/updateProfile" method="post"> <input type="text" name="email"> <button type="submit">更新</button> </form> <!-- 安全:Thymeleaf表单,自动携带Token --> <form th:action="@{/updateProfile}" method="post"> <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/> <input type="text" name="email"> <button type="submit">更新</button> </form>审计时,需要检查提交到敏感POST接口的表单,是否包含了CSRF Token。如果没有,就是一个漏洞点。
3. 检查AJAX请求: 现代前端大量使用AJAX。如果全局启用了CSRF,Spring Security会将Token放在HttpSession的属性中,同时默认也会在Cookie中设置一个名为XSRF-TOKEN的值(可配置)。前端需要将这个Token读取出来,并添加到请求头中(通常是X-XSRF-TOKEN)。
审计时,查看前端JavaScript代码:
// 危险:AJAX请求未添加CSRF Token头 $.ajax({ url: '/api/transfer', type: 'POST', data: {...}, success: function() {...} }); // 安全:从Cookie或Meta标签读取Token并添加到头 var csrfToken = $("meta[name='_csrf']").attr("content"); var csrfHeader = $("meta[name='_csrf_header']").attr("content"); $.ajax({ url: '/api/transfer', type: 'POST', headers: {[csrfHeader]: csrfToken}, // 例如:'X-CSRF-TOKEN': 'abc123...' data: {...}, success: function() {...} });如果发现重要的AJAX POST/PUT/DELETE请求没有处理CSRF Token,就需要标记。
3.3 切入点三:剖析自定义认证与会话管理
一些老系统或特殊需求的系统会有自定义的认证和会话逻辑,这常常是CSRF防护的盲区。
场景:自定义Token认证系统系统可能用自定义的X-Auth-Token头来认证,而不是Cookie。开发人员可能认为“我不依赖Cookie,所以没有CSRF问题”。这是错误的!
如果这个X-Auth-Token是以某种方式存储在浏览器端的(比如Web Storage),恶意页面同样可以通过JavaScript读取到它(如果存在XSS漏洞,则直接读取;如果没有XSS,但Token存储在localStorage中,由于同源策略,evil.com无法读取bank.com的Storage,所以这种情况下是安全的)。关键在于,如果认证凭证可以被恶意页面预测、获取或自动携带,CSRF风险就存在。
审计要点:检查自定义认证机制。如果认证信息(Token、Ticket)是通过请求头传递的,并且这个头不是由浏览器自动携带的(如Cookie、Http Basic Auth弹窗),那么通常可以免疫CSRF。因为恶意页面无法为跨域请求设置自定义头(CORS预检请求会阻止)。但如果这个Token被放在了Cookie里,或者前端代码自动将其添加到每个请求头中,风险依然存在。
3.4 切入点四:验证漏洞可利用性(手工与工具结合)
代码层面怀疑有漏洞,还需要验证是否真的可利用。光看代码有时不够,因为可能有一些业务逻辑上的二次校验。
1. 手工验证流程:
- 步骤1:登录目标应用。
- 步骤2:使用浏览器插件(如
EditThisCookie)查看当前会话Cookie(JSESSIONID)。 - 步骤3:构造一个恶意HTML页面,模拟攻击。例如,针对一个修改邮箱的POST接口:
<!DOCTYPE html> <html> <body> <h1>你收到一张图片!</h1> <!-- GET型POC --> <img src="http://target.com/user/changeEmail?newEmail=attacker@evil.com" style="display:none"/> <!-- POST型POC --> <form id="csrfForm" action="http://target.com/user/updateProfile" method="POST" style="display:none;"> <input type="hidden" name="email" value="hacked@evil.com"/> <!-- 如果原表单有其他必填字段,这里也需要模拟 --> </form> <script>document.getElementById('csrfForm').submit();</script> </body> </html> - 步骤4:在另一个浏览器(或隐身窗口)中打开这个恶意HTML文件。注意,不能在同一浏览器的同一标签页打开,因为那样会话会冲突。理想测试环境是用两台机器,或者一台机器上两个不同的浏览器(如Chrome和Firefox)。
- 步骤5:观察结果。回到原应用,查看邮箱是否被修改。或者查看恶意页面发起的网络请求(F12开发者工具)是否返回成功。
2. 工具辅助:
- Burp Suite:它的
CSRF PoC Generator功能非常强大。在Burp中拦截一个正常的请求(如修改邮箱的POST请求),右键 ->Engagement tools->Generate CSRF PoC。Burp会自动生成一个包含所有表单字段的HTML攻击页面。你可以直接把这个HTML复制出来测试。 - 浏览器扩展:如
CSRF Tester等,可以辅助测试。
重要注意事项:测试CSRF漏洞务必在授权环境下进行,切勿对未授权的生产系统进行测试,这是违法行为。应在测试环境、靶场(如DVWA)或个人搭建的demo中进行。
4. 防御策略深度解析:从框架到代码的纵深防御
发现漏洞后,更重要的是如何修复和防御。防御CSRF不是简单加个Token,而是一个体系。
4.1 同步器令牌模式(Synchronizer Token Pattern)及其实现
这是最主流、最有效的防御方式,Spring Security的CSRF防护核心就是它。原理很简单:服务器在用户会话中生成一个随机的、不可预测的令牌(Token),在渲染表单(或页面)时将这个令牌输出。当用户提交表单时,必须将这个令牌一并提交回来。服务器校验提交的令牌是否与会话中存储的一致。
Spring Security的实现细节:
- Token生成与存储:默认使用
HttpSessionCsrfTokenRepository。当请求一个页面时,CsrfFilter会检查Session中是否存在CSRFToken(属性名默认为org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN)。如果没有,则生成一个(默认是UUID),存入Session。 - Token传递到前端:
- 对于表单:通过
RequestAttributes暴露给视图。在JSP中,可以通过${_csrf.token}获取值,通过${_csrf.parameterName}获取参数名(默认为_csrf)。 - 对于Cookie:默认配置下,
CsrfTokenRepository还会将Token写入一个名为XSRF-TOKEN的Cookie中。这是为了便于前端JavaScript框架(如AngularJS)自动读取并添加到请求头。
- 对于表单:通过
- Token提交与校验:客户端提交时,必须将Token放在:
- 参数(Parameter)中:对于表单提交,通常是隐藏域
<input type=”hidden” name=”_csrf” value=”token-value”/>。 - 请求头(Header)中:对于AJAX请求,通常是
X-CSRF-TOKEN或X-XSRF-TOKEN头。CsrfFilter会拦截POST,PUT,PATCH,DELETE等请求(默认配置),提取客户端提交的Token,并与Session中存储的Token进行比对。
- 参数(Parameter)中:对于表单提交,通常是隐藏域
自定义TokenRepository:如果默认的Session存储不符合需求(比如集群会话共享),可以实现CsrfTokenRepository接口,将Token存储在Redis等分布式缓存中。
4.2 双重Cookie验证的利与弊
另一种常见思路是“双重Cookie验证”。流程如下:
- 前端在请求时,从Cookie中读取某个自定义的Token(例如
CSRF-TOKEN)。 - 前端将这个Token作为参数或请求头(如
X-CSRF-TOKEN)附加到请求中。 - 后端比较请求中的Token和Cookie中的Token是否一致。
听起来和同步器令牌很像?关键区别在于Token的存储和传递逻辑。在同步器令牌模式中,Token是服务器生成并存储在服务器端(Session),下发给客户端,客户端再提交回来验证。在双重Cookie验证中,Token是服务器通过Set-Cookie下发,存储在客户端浏览器,客户端在请求时手动将其从Cookie中取出并放到另一个位置(参数或头)提交。
弊端:
- 如果网站存在XSS漏洞,此方案完全失效。因为XSS攻击可以读取到Cookie中的Token。
- 需要依赖前端JavaScript主动读取和设置,增加了前端复杂度。
- 对Cookie的属性有要求,最好设置为
HttpOnly=false以便JS读取,但这又降低了Cookie本身的安全性(虽然CSRF Token本身不是敏感凭证,但最好也不要用作他途)。
因此,在Java生态中,优先推荐使用Spring Security内置的同步器令牌模式,它更成熟、集成度更高。
4.3 检查Referer/Origin头的补充防御
这是一种辅助手段,不能作为唯一防御。原理是检查HTTP请求头中的Referer(或Origin)字段,看请求是否来源于本站点。
在Spring中实现:可以自定义一个Filter,放在安全链中。
public class RefererCheckFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String referer = request.getHeader("Referer"); String origin = request.getHeader("Origin"); String serverName = request.getServerName(); // 对于状态变更请求进行检查 if ("POST".equalsIgnoreCase(request.getMethod()) || ...) { boolean valid = false; if (referer != null && referer.contains(serverName)) { valid = true; } else if (origin != null && origin.contains(serverName)) { valid = true; } if (!valid) { response.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid request source"); return; } } filterChain.doFilter(request, response); } }为什么只能作为补充:
- Referer可能被篡改或缺失:一些浏览器插件或安全设置会清除或修改Referer。从HTTPS页面跳转到HTTP页面,Referer也可能被剥离。合法的场景下也可能没有Referer。
- Origin头仅存在于CORS请求中:对于简单的表单提交,没有Origin头。
- 存在绕过历史:如果验证逻辑不严谨(比如只用
contains检查域名,攻击者可以注册一个包含你域名的子域名,如www.bank.com.attacker.com),可能被绕过。
所以,Referer/Origin检查应该与CSRF Token结合使用,作为深度防御的一环,用于增加攻击门槛。
4.4 关键操作增加二次认证
对于特别敏感的操作,如转账、修改密码、修改绑定手机号等,除了CSRF Token,应强制要求用户进行二次认证。例如:
- 输入当前密码(或支付密码)。
- 输入短信验证码。
- 进行生物识别(指纹、人脸)。
这属于业务逻辑层面的加固。即使CSRF攻击成功,攻击者也无法提供二次认证凭证,从而阻止操作。在审计时,要重点关注这些核心敏感功能是否有此环节。
5. 审计案例复盘:从真实漏洞中学习
理论说再多,不如看几个我实际审计中遇到的“活生生”的案例。
5.1 案例一:Spring Security配置疏漏导致全局CSRF防护关闭
项目背景:一个中型电商平台,使用Spring Boot 2.3 + Spring Security。漏洞代码:
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/static/**", "/login").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .permitAll() .and() .logout() .permitAll() .and() // 开发者为了在开发阶段方便测试API,添加了这行 .csrf().disable(); // 致命错误:全局禁用CSRF } }审计发现过程:在审查安全配置类时,一眼就看到了.csrf().disable()。询问开发团队,理由是“前端是Vue,用了JWT,觉得不需要”。但进一步检查发现,该应用并非纯前后端分离。仍有部分管理后台页面使用JSP渲染,表单提交到/admin/product/update等接口。这些接口依然依赖Session Cookie认证。风险:攻击者可以构造恶意页面,诱使已登录的管理员访问,从而静默修改商品价格、下架商品等。修复建议:
- 删除
.csrf().disable(),启用默认防护。 - 对于纯API接口(由Vue前端通过AJAX调用且使用JWT),配置
.ignoringAntMatchers(“/api/v1/**”)。但需要确保/api/v1/**下的端点确实只被Vue前端使用,且Vue前端通过Axios拦截器在请求头中正确添加了JWTAuthorization头,而不是依赖Cookie。 - 对于管理后台的JSP页面,确保表单使用了Spring的
<form:form>标签或手动添加了_csrf隐藏域。
5.2 案例二:AJAX请求遗漏CSRF Token处理
项目背景:一个社交网站,使用Spring MVC + jQuery,启用了CSRF防护。漏洞代码(前端):
// 用户关注某个用户的AJAX请求 function followUser(userId) { $.ajax({ url: '/action/follow', type: 'POST', contentType: 'application/json', data: JSON.stringify({targetId: userId}), success: function(data) { alert('关注成功!'); } }); }审计发现过程:在审查前端JS文件时,发现大量的$.ajaxPOST请求。随机抽查几个关键操作(关注、点赞、收藏),发现都没有设置CSRF Token相关的请求头。检查页面HTML,发现<meta>标签里确实有Token信息(说明后端生成了),但前端JS没有去读取和使用。风险:攻击者可以构造一个页面,包含自动执行followUser(attackerUserId)的脚本。已登录的用户访问后,就会在不知情下关注攻击者。修复建议:
- 全局配置jQuery的
ajaxSetup,自动添加CSRF Token头。$(document).ajaxSend(function(event, xhr, settings) { if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type)) { var token = $("meta[name='_csrf']").attr("content"); var header = $("meta[name='_csrf_header']").attr("content"); if (token && header) { xhr.setRequestHeader(header, token); } } }); - 或者,如果后端配置了将Token放在Cookie(
XSRF-TOKEN),并且前端使用了如Axios等库,它们可能支持自动从Cookie读取并设置头。
5.3 案例三:错误豁免了敏感API接口
项目背景:一个混合架构的应用,既有传统页面,也有为移动App提供的REST API。漏洞代码:
@Override protected void configure(HttpSecurity http) throws Exception { http .csrf() .ignoringAntMatchers("/api/**") // 意图是豁免所有API .and() .authorizeRequests() .antMatchers("/api/auth/**").permitAll() .antMatchers("/api/**").authenticated(); // API需要认证 }审计发现过程:审计配置时,发现/api/**被豁免了CSRF检查。理由是“API使用Token认证”。但深入检查发现,认证方式是基于Session的!移动App确实用了Token,但部分/api/user/updateInfo接口也被Web端的管理页面调用,而管理页面使用的是Cookie-Session认证。风险:/api/user/updateInfo这个接口,既可以被移动App(带Token头)调用,也可以被浏览器(带Cookie)调用。由于它被豁免了CSRF检查,当通过浏览器Cookie访问时,就暴露在CSRF风险下。修复建议:
- 精细化豁免:不要粗暴地豁免整个
/api/**。只豁免真正纯API、使用非Cookie认证的端点。例如:.ignoringAntMatchers(“/api/mobile/**”),并为移动端接口配置独立的、基于Token的认证过滤器。 - 分离路径:将面向浏览器和面向客户端的API彻底分开。例如,浏览器端用
/webapi/**,移动端用/mobileapi/**。这样在安全配置上可以区别对待。 - 统一认证方式:如果可能,将所有接口的认证方式统一。例如,全部改用JWT,并且在Web前端将JWT存储在
HttpOnly的Cookie中(需妥善处理CSRF),或者存储在内存中并通过拦截器添加到头。
6. 进阶:CSRF与现代化架构的碰撞
随着前后端分离、微服务、API网关的普及,CSRF的防御场景也在变化。
6.1 前后端分离(SPA)下的CSRF防护
在单页面应用(SPA,如Vue、React)中,后端通常只提供JSON API,前端通过AJAX调用。此时,CSRF防护的关键在于Token如何传递和存储。
方案A:继续使用Spring Security默认机制
- 后端:保持CSRF启用。
- 前端:在首次访问或登录后,前端需要从后端获取CSRF Token。Spring Security可以通过以下方式提供:
- 在某个初始化接口的响应头中返回。
- 通过一个安全的GET端点(如
/csrf-token)返回。 - 利用Cookie(设置
XSRF-TOKEN),前端JS读取。
- 前端存储:将Token存储在内存(如Vuex、Redux)或Web Storage中。
- 请求发送:在发起非幂等的AJAX请求(POST, PUT, DELETE)时,将Token添加到请求头(如
X-XSRF-TOKEN)。
方案B:使用JWT等无状态认证,并妥善处理
- 如果API完全使用JWT,且JWT是通过
Authorization: Bearer <token>头传递(而不是放在Cookie里),那么从技术上讲,CSRF攻击无法伪造这个自定义头,因此可以免疫CSRF。 - 但是!如果JWT被存储在
localStorage或sessionStorage中,前端JS需要读取它并加到请求头。这本身是安全的(因为同源策略保护了Storage)。最大的风险是XSS:一旦存在XSS漏洞,攻击者脚本可以窃取Storage中的JWT。因此,选择此方案必须搭配严格的XSS防护。 - 更佳实践:将JWT存储在
HttpOnly的Cookie中(防止XSS窃取),然后通过一些方式防御CSRF(如SameSite Cookie属性,或额外的CSRF Token)。这又回到了方案A的思路上。
SameSite Cookie属性:这是一个重要的浏览器安全特性。将Cookie设置为SameSite=Strict或SameSite=Lax,可以很大程度上阻止CSRF攻击。
Strict:Cookie仅在同站请求(即当前站点)中发送。从其他站点过来的链接、表单提交都不会携带此Cookie。Lax:比Strict宽松一些,允许从外部站点导航到该站点时(如点击链接)携带Cookie,但禁止在跨站POST提交或嵌入资源(如图片、iframe)加载时携带。 在Spring Security中,可以通过http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())来设置CSRF Token Cookie的SameSite属性(需配合Servlet容器配置)。对于会话Cookie(JSESSIONID),需要在应用服务器(如Tomcat)或反向代理(如Nginx)层面进行配置。
6.2 微服务与API网关场景
在微服务架构下,CSRF的防御责任上移到了API网关或边缘服务。
- 统一认证网关:所有请求先经过网关。网关负责会话管理、生成和校验CSRF Token。下游微服务无需关心CSRF,它们只接收来自网关的、已认证的内部请求。
- 挑战:需要确保网关到微服务之间的通信是可信的(通常在内网),并且网关能够正确地将用户身份信息(如User ID)传递给下游服务。
- 审计重点:在这种情况下,代码审计的重点就从每个微服务转移到了网关的安全配置上。需要审计网关的CSRF防护模块是否正确启用和配置。
7. 自动化审计辅助与 checklist
对于大型项目,完全依赖人工审计效率低。可以结合自动化工具和清单。
1. 静态代码分析(SAST)工具:
- Find Security Bugs、SonarQube:可以扫描Java代码,识别出可能缺少CSRF防护的Controller方法(例如,检测到
@PostMapping但未在方法参数或类级别发现@RequestMapping等与CSRF相关的注解或校验代码)。这些工具能提供线索,但需要人工复核误报。 - 自定义规则:可以编写Checkstyle或PMD规则,检查所有
@PostMapping,@PutMapping,@DeleteMapping注解的方法,是否在对应的JSP/Thymeleaf模板中存在表单,且表单中是否包含名为_csrf的输入域(这需要关联前后端代码分析,比较复杂)。
2. 动态应用测试(DAST)工具:
- Burp Suite Professional, OWASP ZAP:这些渗透测试工具可以自动探测CSRF漏洞。它们会爬取网站,识别所有表单,然后尝试移除或篡改潜在的CSRF Token参数,重放请求,观察响应是否成功。这是验证漏洞可利用性的有效手段。
3. Java代码审计Checklist(CSRF专项): 你可以拿着下面这个清单去审查项目:
| 检查项 | 检查点 | 预期结果/安全实践 |
|---|---|---|
| 全局配置 | Spring Security配置中,http.csrf()是否被显式启用或未禁用? | 应启用(或至少未调用.disable()) |
如果使用了.ignoringAntMatchers(),排除的路径是否合理? | 仅排除登录、注销、公开的无状态API | |
| 接口层面 | 所有执行状态变更的Controller方法(POST/PUT/PATCH/DELETE)是否都要求CSRF Token? | 是。除非有充分理由豁免 |
| 是否存在用GET方法执行修改操作的接口? | 应重构为POST等方法,并添加CSRF防护 | |
| 前端页面 | 所有表单(包括HTML原生和框架标签)是否都包含了CSRF Token字段? | 是。检查JSP/Thymeleaf模板 |
| 所有重要的AJAX请求(POST/PUT/DELETE)是否在请求头中携带了CSRF Token? | 是。检查前端JavaScript代码 | |
| 会话与认证 | 如果使用自定义认证(非Session),认证凭证是否会被浏览器自动携带? | 理想情况不应被自动携带(如自定义头) |
会话Cookie是否考虑设置SameSite属性? | 建议设置为Lax或Strict | |
| 敏感操作 | 对于关键操作(转账、改密),是否有二次认证? | 应有密码、短信验证码等二次确认 |
CSRF是一个经典的、原理简单但危害巨大的漏洞。在Java Web安全审计中,它往往不是最难发现的,却最容易因为开发人员的“想当然”和配置疏忽而出现。防御的核心在于理解“状态变更请求必须验证来源”这一原则,并善用框架提供的成熟机制(如Spring Security的CSRF防护)。同时,要建立纵深防御的思想,结合Token、SameSite Cookie、Referer检查、二次认证等多种手段。审计时,从安全配置入手,顺着请求流梳理,重点关注配置豁免、前端遗漏、接口误用这几个高频漏洞点,就能系统性地将CSRF风险降到最低。
