Spring Security正则认证绕过漏洞CVE-2022-22978深度剖析
1. 项目概述:一次由正则表达式“点号”引发的认证绕过
最近在复盘一些经典的Spring Security安全漏洞时,CVE-2022-22978这个编号又跳了出来。这个漏洞的原理说起来并不复杂,甚至有点“眼熟”——它和之前分析过的Apache Shiro的CVE-2022-32532在核心思路上如出一辙。但正是这种“似曾相识”,让我觉得有必要把它掰开揉碎了讲清楚。这不仅仅是为了复现一个漏洞,更是为了理解一种在Web安全中反复出现的攻击模式:如何利用框架或语言特性的微小差异,在看似严密的认证逻辑上撕开一道口子。
简单来说,CVE-2022-22978是一个影响特定版本Spring Security的认证绕过漏洞。当开发者使用 RegexRequestMatcher 这个组件,并配置了包含英文句点“.”的正则表达式来匹配受保护路径时,攻击者可以通过在请求路径中插入特定的换行符编码(如 %0a 或 %0d ),成功绕过认证检查,直接访问本应需要登录才能查看的资源。受影响的版本包括Spring Security 5.5.x < 5.5.7 以及 5.6.x < 5.6.4。这个漏洞的危险性在于,它绕过了Spring Security默认的严格防火墙( StrictHttpFirewall ),利用了正则表达式引擎与HTTP路径解析之间一个容易被忽略的“认知偏差”。
对于应用安全工程师、红队人员以及使用Spring Security的Java后端开发者而言,理解这个漏洞至关重要。它警示我们,即使使用了成熟的安全框架,如果对底层组件的交互细节理解不足,仍然可能引入严重的安全隐患。接下来,我将从环境搭建、原理深度剖析、漏洞复现、修复方案以及由此引发的关于自动化漏洞挖掘的思考几个方面,带你完整走一遍这个漏洞的分析之旅。
2. 漏洞原理深度剖析:当正则“点号”遇上路径解析
要理解CVE-2022-22978,我们不能孤立地只看Spring Security的代码,而需要将其置于一个更大的上下文:HTTP请求的处理链条、正则表达式的匹配规则以及不同框架组件对URL的解析方式。这个漏洞的本质,是 RegexRequestMatcher 的匹配逻辑与 HttpServletRequest.getServletPath() 的路径净化行为之间,出现了一个微妙的不匹配。
2.1 核心冲突点: getServletPath() 的“净化”与正则的“严格”
漏洞的根源在于 org.springframework.security.web.util.matcher.RegexRequestMatcher#matches 方法。为了进行路径匹配,这个方法会调用 request.getServletPath() 来获取请求的路径。 getServletPath() 这个方法有一个重要的行为:它会对URL进行解码(URL Decode),并且会移除路径中分号 ; 之后直到下一个斜杠 / 之前的内容。这个设计初衷是为了处理JSR规范中关于“路径参数”(Path Parameters)的情况,但在安全上下文中,它无意间扮演了一个“路径规范化”的角色。
假设我们配置了这样一个安全规则:使用正则表达式 ^/admin/.* 来保护所有以 /admin/ 开头的路径。在开发者的直觉里, .* 中的点号 . 应该匹配任何字符(除了换行符)。当攻击者请求 /admin/1%0a ( %0a 是换行符 \n 的URL编码)时, getServletPath() 在处理后,返回的路径字符串仍然是 /admin/1\n 。注意,这里的 \n 是一个字面意义上的换行符,而不是一个转义序列。
现在,关键来了。这个包含了换行符的路径字符串 /admin/1\n 被送入正则表达式引擎进行匹配。在Java默认的正则表达式引擎中,元字符 . (点号)的默认行为是匹配 除换行符( \n , \r )之外 的任何单个字符。因此,正则表达式 ^/admin/.* 中的 .* 无法匹配到路径末尾的换行符 \n 。这就导致整个正则匹配失败。
注意 :这里有一个非常重要的细节。
getServletPath()返回的是解码后的字符串,其中的%0a已经变成了一个真正的\n字符(ASCII码10)。正则引擎在处理字符串时,看到的就是这个实际的换行符,而不是%0a这个百分号编码。匹配失败是因为.不匹配\n这个字符,而不是因为.不匹配%0a这个字符串。
匹配失败意味着什么?在Spring Security的权限判断链中, RegexRequestMatcher 返回 false ,表示当前请求 不匹配 这条需要认证的规则。因此,安全过滤器就会放行这个请求,认为它不需要认证,从而实现了绕过。
2.2 为何传统路径遍历攻击无效?
有经验的安全研究员可能会立刻想到另一个常见的绕过手法:使用路径遍历序列,比如 /admin/..;/1 。这种手法的原理是利用 getServletPath() 或类似方法对 ; 的处理,将 ..; 部分移除,使得最终用于匹配的路径变成了 /admin/1 ,从而可能绕过对 /admin/..;/1 的检查。
然而,在Spring Security中,这条路被 StrictHttpFirewall 堵死了。 StrictHttpFirewall 是Spring Security 5.x之后引入的默认HTTP防火墙,它会主动拒绝包含 ; 、 // 、 \ 、 %2e%2e ( .. )等可疑字符的请求。因此,试图通过 ..; 进行绕过的请求在到达 RegexRequestMatcher 之前就会被防火墙拦截并返回400错误。这也反衬出CVE-2022-22978利用换行符的巧妙之处—— %0a 和 %0d 在当时并未被 StrictHttpFirewall 默认拦截,从而成功穿过了第一道防线。
2.3 与CVE-2022-32532的“孪生”关系
如果你分析过Apache Shiro的CVE-2022-32532漏洞,此刻一定会感到强烈的既视感。那个漏洞发生在Shiro的 RegExPatternMatcher 中,原理几乎一模一样:也是因为Shiro在路径匹配时,某个环节对URL进行了解码,然后将包含换行符的路径与一个包含 . 的正则表达式进行匹配,同样因为 . 不匹配换行符而导致匹配失败,进而绕过认证。
这种跨框架的“孪生漏洞”现象在安全领域并不罕见。它揭示了一个深刻的道理:许多漏洞并非某个框架独有的“Bug”,而是源于编程语言、协议规范或通用编程模式中的固有特性。当多个框架都基于相同的特性(如Java正则 . 的匹配规则、HTTP URL解码规范)实现相似的功能(如路径匹配)时,就可能在相同的位置犯下相似的安全错误。这也为我们的漏洞挖掘提供了清晰的思路:在一个框架中发现由某种特性引发的漏洞后,应立即思考其他使用相同特性或模式的框架是否也存在同类问题。
3. 漏洞环境搭建与复现实操
理论分析之后,我们需要一个真实的环境来验证和理解。手动搭建一个漏洞环境能让你对漏洞的触发条件有最直观的感受。
3.1 环境准备与项目导入
最便捷的方式是使用GitHub上现成的漏洞环境。这里我推荐使用一个专为漏洞复现设计的Spring Boot项目。打开终端,执行以下命令:
git clone https://github.com/XuCcc/VulEnv.git
cd VulEnv/springboot/cve_2022_22978
这个项目结构非常清晰,是一个标准的Spring Boot应用。使用你熟悉的IDE(如IntelliJ IDEA或Eclipse)导入这个Maven项目。关键点在于 pom.xml 文件,它故意引入了存在漏洞的Spring Security版本:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.6.6</version> <!-- 此版本默认依赖Spring Security 5.6.3,正在受影响范围内 -->
</dependency>
确认依赖后,查看核心的安全配置类,通常命名为 SecurityConfig 。你会看到类似以下的配置,这正是触发漏洞的关键:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// 漏洞点:使用RegexRequestMatcher,且正则中包含“.”
.requestMatchers(new RegexRequestMatcher("^/admin/.*", null))
.authenticated()
.anyRequest().permitAll()
.and()
.formLogin();
}
}
同时,会有一个简单的 AdminController ,提供一个需要认证的端点:
@RestController
public class AdminController {
@GetMapping("/admin/{name}")
public String adminAccess(@PathVariable String name) {
return "Hello Admin, you accessed: " + name;
}
}
启动应用,访问 http://localhost:8080/admin/test ,你会被重定向到登录页面(如 http://localhost:8080/login ),这说明安全规则生效了。
3.2 漏洞复现过程与细节
现在,我们开始攻击。漏洞利用非常简单,只需要在浏览器地址栏、 curl 命令或Burp Suite等工具中,修改请求的URL。
正常请求(被拦截):
GET /admin/test HTTP/1.1
Host: localhost:8080
响应:302重定向到 /login 。
绕过请求(利用漏洞):
GET /admin/test%0a HTTP/1.1
Host: localhost:8080
或者使用 curl 命令:
curl -v http://localhost:8080/admin/test%0a
此时,观察响应。你应该会直接看到 Hello Admin, you accessed: test 的输出,而不会再被重定向到登录页。这意味着认证被成功绕过。
实操心得 :在测试时,务必注意你的测试工具是否会自动对URL进行二次编码。例如,如果你在Burp Suite的Repeater模块中手动输入
%0a,Burp默认可能会将其作为字面字符发送。最可靠的方式是使用Burp的“Paste from file”功能,或者使用curl的--raw参数来确保换行符编码被正确发送。此外,除了%0a(LF,\n),也可以尝试%0d(CR,\r),原理相同。
背后的数据流验证: 为了更深刻地理解,你可以在 RegexRequestMatcher.matches 方法处打一个断点(在 spring-security-web-5.6.3.jar 中)。分别用 /admin/test 和 /admin/test%0a 发起请求,观察 request.getServletPath() 的返回值:
- 对于前者,返回
/admin/test。 - 对于后者,返回
/admin/test\n(这里\n显示为一个不可见字符,在调试器里可能显示为一个小方块或空格,但其ASCII码是10)。
接着,观察正则匹配的结果。对于路径 /admin/test\n ,正则 ^/admin/.* 会尝试匹配。由于 .* 无法“吃掉”末尾的 \n ,整个匹配失败,返回 false ,请求遂被放行。
4. 漏洞修复方案与安全配置建议
Spring官方在收到报告后,迅速发布了修复版本(Spring Security 5.5.7 和 5.6.4)。修复方案直指问题核心。
4.1 官方修复代码分析
修复位于 RegexRequestMatcher 类中。官方并没有修改正则表达式引擎的行为,也没有改变 getServletPath() 的语义,而是采用了一种更安全、更彻底的策略: 在将路径送入正则引擎匹配之前,主动移除路径中的换行符 。
查看修复后的源码(以5.6.4为例),在 matches 方法中,你会看到类似如下的逻辑:
public boolean matches(HttpServletRequest request) {
String url = request.getServletPath();
String pathInfo = request.getPathInfo();
// ... 合并url和pathInfo ...
String fullPath = url + (pathInfo == null ? "" : pathInfo);
// 关键修复:移除所有换行符和回车符
fullPath = StringUtils.deleteAny(fullPath, "\n\r");
// 然后再进行正则匹配
return this.pattern.matcher(fullPath).matches();
}
StringUtils.deleteAny(fullPath, "\n\r") 这一行代码,就是整个修复的灵魂。它确保在匹配之前,路径字符串中不可能存在 \n 或 \r 字符,从根本上杜绝了利用换行符进行不匹配的可能性。
修复方案的优劣评析:
- 优点 :修复方案简单、直接、有效。它没有引入复杂的逻辑,也没有破坏原有API的兼容性,属于“最小化修复”的典范。同时,它在
RegexRequestMatcher内部处理,对上层应用透明,开发者无需修改任何业务代码或安全配置。 - 潜在考虑 :这种修复方式隐含地承认了“路径中包含换行符是异常且应被拒绝的”。这其实与
StrictHttpFirewall的设计哲学一脉相承。也许在未来,StrictHttpFirewall的默认拒绝列表会直接加入%0a和%0d。
4.2 对于开发者的升级与配置建议
如果你的项目正在使用受影响的Spring Security版本,首要且最紧急的行动就是升级依赖。
对于Maven项目:
<!-- 升级到安全版本 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.6.4</version> <!-- 或 5.5.7 -->
</dependency>
升级后,务必运行完整的测试套件,确保升级没有引入意外的回归问题。
如果暂时无法升级? 虽然强烈建议升级,但在极端情况下,可以考虑以下临时缓解措施:
- 自定义
RequestMatcher:避免使用RegexRequestMatcher,尤其是包含.的通配符正则。改用更精确的AntPathRequestMatcher(它使用Ant风格路径,如/admin/**),其实现逻辑不同,不受此漏洞影响。.requestMatchers(new AntPathRequestMatcher("/admin/**")) - 强化
StrictHttpFirewall:自定义防火墙规则,主动拒绝包含换行符编码的请求。@Bean public StrictHttpFirewall strictHttpFirewall() { StrictHttpFirewall firewall = new StrictHttpFirewall(); // 设置允许的HTTP方法等... // 虽然5.6.3版本默认未拦截,但我们可以自定义一个更严格的验证 // 注意:此方法在旧版本中可能无法直接拦截%0a,但可以作为一种深度防御。 return firewall; }注意 :在漏洞版本中,
StrictHttpFirewall默认不拦截%0a和%0d,所以自定义防火墙主要起补充作用,不能完全替代升级。
长期安全配置建议:
- 最小权限原则 :安全规则应尽可能精确。避免使用
.*这种过于宽泛的正则,除非确有必要。优先使用具体的路径模式。 - 纵深防御 :不要仅仅依赖Web框架的安全机制。在重要的业务接口(尤其是管理员接口)中,应在Controller或Service层再次进行权限校验。
- 关注安全公告 :订阅Spring官方安全公告,及时了解依赖组件的漏洞信息。
5. 漏洞挖掘的延伸思考:从个案到模式
分析完CVE-2022-22978,我们不能仅仅停留在“这个漏洞是怎么回事”的层面。作为一名安全研究员或关注安全的开发者,更应该思考的是: 如何从这一个漏洞,发现一类漏洞? 这个漏洞为我们提供了一个绝佳的“漏洞挖掘模式”案例。
5.1 高效的漏洞挖掘方法论:特性迁移与对比审计
CVE-2022-22978和CVE-2022-32532的“孪生”关系,揭示了一种高效的漏洞挖掘方法: 特性迁移与对比审计 。
- 定位核心特性 :首先,深入理解漏洞的核心触发特性。在这个案例中,特性是“Java正则表达式默认模式下,元字符
.不匹配换行符\n和\r”。 - 寻找应用场景 :接着,寻找该特性在软件中的具体应用场景。这里的关键场景是“ 使用正则表达式进行用户输入(特别是URL路径)匹配,以做出安全决策(如认证、授权) ”。
- 框架/组件审计 :然后,带着这个“特性-场景”对,去审计其他流行的框架、库或自定义代码。问自己:还有哪些框架用正则匹配路径?它们处理URL解码和路径规范化的顺序是怎样的?匹配逻辑是否可能因为
.不匹配换行符而出错? - 构造利用链 :最后,如果找到了潜在的目标,构造完整的利用链。包括:如何让换行符进入匹配字符串(通常需要URL解码)、匹配失败是否会导致安全逻辑被绕过(通常是认证或授权)。
这种方法将漏洞挖掘从“漫无目的的黑盒测试”转变为“有理论指导的白盒/灰盒审计”,极大地提高了效率。你可以将“正则 . 不匹配换行符”这个特性,应用到任何使用正则进行安全匹配的上下文中,例如某些WAF(Web应用防火墙)的规则匹配、自定义的访问控制中间件、路由框架的路径映射等。
5.2 API安全与自动化Fuzz的启示
原文提到了API安全和自动化Fuzz,这确实是当前漏洞挖掘的一个重要方向。CVE-2022-22978这类漏洞,非常适合作为自动化API Fuzz的检测目标。
为什么API Fuzz有效?
- 接口标准化 :现代API(如RESTful API)有相对固定的结构(路径、方法、参数),易于程序化遍历和测试。
- 输入向量明确 :路径(Path)、查询参数(Query)、请求体(Body)、头部(Header)都是明确的输入点。
- 逻辑漏洞高发区 :认证绕过、权限提升、业务逻辑漏洞经常出现在API接口中。
针对此类漏洞的Fuzz思路: 你可以设计一个简单的Fuzz脚本,针对配置了正则表达式权限的端点进行测试:
- 识别目标 :通过静态分析或动态爬取,找出应用中所有使用了
RegexRequestMatcher或类似机制的安全配置。 - 生成Payload :对于每个受保护路径,生成一系列变异Payload,核心就是在路径末尾或参数中插入
%0a,%0d,%0a%0d等编码。 - 发送请求与判断 :发送变异后的请求,关键不是看响应状态码(可能是200),而是看响应内容是否与未授权访问时不同(例如,是否返回了本应登录后才能看到的数据)。
- 结果验证 :对疑似绕过的请求,进行多次验证,排除缓存等干扰因素。
一个简化的概念性Python脚本示例如下:
import requests
target_url = "http://target-app/admin/{}"
protected_path = "admin"
fuzz_payloads = ["%0a", "%0d", "%0a%0d", "/%0a", "/%0d"]
for payload in fuzz_payloads:
test_url = target_url.format(payload)
resp = requests.get(test_url, allow_redirects=False) # 禁止重定向,直接观察响应
# 判断逻辑:如果响应不是跳转到登录页(如302到/login),且包含了后台数据,则可能绕过成功
if resp.status_code == 200 and "Hello Admin" in resp.text:
print(f"[!] Potential bypass found with payload: {payload}")
print(f" URL: {test_url}")
这个脚本非常简单,真实的Fuzz工具会更复杂,需要处理会话、令牌、更智能的差异对比等。但核心思想是一致的:自动化地测试各种边界情况,寻找安全逻辑的裂缝。
5.3 漏洞研究中的“灵感”与“运气”
最后,我想谈谈漏洞挖掘中的“灵感”。很多人觉得找漏洞靠运气,但像CVE-2022-22978这种漏洞的发现,更多是源于对技术的深刻理解和有方向的思考。发现者很可能是在研究Shiro漏洞(CVE-2022-32532)时,立刻想到了:“Spring Security里是不是也有类似的组件?它的实现会不会有同样的问题?” 这种联想能力,建立在扎实的基础知识和广泛的框架了解之上。
所以,持续学习、阅读其他漏洞的分析报告、理解底层原理(如HTTP协议、正则引擎、语言特性),比盲目地运行扫描器更能产生高质量的漏洞发现。自动化Fuzz是你的“矛”和“网”,可以覆盖大量测试面;而深入的技术理解和联想能力是你的“雷达”和“地图”,能指引你前往最可能发现宝藏的地方。两者结合,才是漏洞挖掘的正确姿势。
更多推荐




所有评论(0)