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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId version2.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模块中手动输入%0aBurp默认可能会将其作为字面字符发送。最可靠的方式是使用Burp的“Paste from file”功能或者使用curl的--raw参数来确保换行符编码被正确发送。此外除了%0aLF,\n也可以尝试%0dCR,\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 groupIdorg.springframework.security/groupId artifactIdspring-security-web/artifactId version5.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解码、匹配失败是否会导致安全逻辑被绕过通常是认证或授权。这种方法将漏洞挖掘从“漫无目的的黑盒测试”转变为“有理论指导的白盒/灰盒审计”极大地提高了效率。你可以将“正则.不匹配换行符”这个特性应用到任何使用正则进行安全匹配的上下文中例如某些WAFWeb应用防火墙的规则匹配、自定义的访问控制中间件、路由框架的路径映射等。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_redirectsFalse) # 禁止重定向直接观察响应 # 判断逻辑如果响应不是跳转到登录页如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是你的“矛”和“网”可以覆盖大量测试面而深入的技术理解和联想能力是你的“雷达”和“地图”能指引你前往最可能发现宝藏的地方。两者结合才是漏洞挖掘的正确姿势。