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>

升级后,务必运行完整的测试套件,确保升级没有引入意外的回归问题。

如果暂时无法升级? 虽然强烈建议升级,但在极端情况下,可以考虑以下临时缓解措施:

  1. 自定义 RequestMatcher :避免使用 RegexRequestMatcher ,尤其是包含 . 的通配符正则。改用更精确的 AntPathRequestMatcher (它使用Ant风格路径,如 /admin/** ),其实现逻辑不同,不受此漏洞影响。
    .requestMatchers(new AntPathRequestMatcher("/admin/**"))
    
  2. 强化 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的“孪生”关系,揭示了一种高效的漏洞挖掘方法: 特性迁移与对比审计

  1. 定位核心特性 :首先,深入理解漏洞的核心触发特性。在这个案例中,特性是“Java正则表达式默认模式下,元字符 . 不匹配换行符 \n \r ”。
  2. 寻找应用场景 :接着,寻找该特性在软件中的具体应用场景。这里的关键场景是“ 使用正则表达式进行用户输入(特别是URL路径)匹配,以做出安全决策(如认证、授权) ”。
  3. 框架/组件审计 :然后,带着这个“特性-场景”对,去审计其他流行的框架、库或自定义代码。问自己:还有哪些框架用正则匹配路径?它们处理URL解码和路径规范化的顺序是怎样的?匹配逻辑是否可能因为 . 不匹配换行符而出错?
  4. 构造利用链 :最后,如果找到了潜在的目标,构造完整的利用链。包括:如何让换行符进入匹配字符串(通常需要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脚本,针对配置了正则表达式权限的端点进行测试:

  1. 识别目标 :通过静态分析或动态爬取,找出应用中所有使用了 RegexRequestMatcher 或类似机制的安全配置。
  2. 生成Payload :对于每个受保护路径,生成一系列变异Payload,核心就是在路径末尾或参数中插入 %0a , %0d , %0a%0d 等编码。
  3. 发送请求与判断 :发送变异后的请求,关键不是看响应状态码(可能是200),而是看响应内容是否与未授权访问时不同(例如,是否返回了本应登录后才能看到的数据)。
  4. 结果验证 :对疑似绕过的请求,进行多次验证,排除缓存等干扰因素。

一个简化的概念性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是你的“矛”和“网”,可以覆盖大量测试面;而深入的技术理解和联想能力是你的“雷达”和“地图”,能指引你前往最可能发现宝藏的地方。两者结合,才是漏洞挖掘的正确姿势。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐