CVE-2026-41843 Spring Framework路径遍历漏洞浅析
漏洞分析
当 Spring MVC 或 Spring WebFlux 应用配置了从文件系统提供版本化静态资源时,在受影响版本中,攻击者可通过构造包含路径遍历序列的恶意请求,解析配置目录之外的文件。
0x00 CVE-2026-41843 ===================  **主要影响范围:** Spring Framework: - 7.0.0 - 7.0.7 - 6.2.0 - 6.2.18 - 6.1.0 - 6.1.27 - 5.3.0 - 5.3.48 以及不再支持维护的版本同样受到影响。 0x01 Spring中的**资源版本控制** ======================= 静态资源缓存是 Web 性能优化的基础手段。 浏览器会长期缓存 CSS、JS、图片等静态资源,避免重复请求。但当服务端资源更新时,如果 URL 保持不变,浏览器会继续使用本地缓存,导致用户无法获取最新内容。 资源版本控制(也叫缓存击穿 / Cache Busting)就是为了解决这个问题:在静态资源的 URL 中嵌入版本标识,资源更新时版本标识同步变化,让浏览器将其视为全新的资源 URL,强制重新加载。 Spring Framework 提供了标准化的静态资源版本控制能力,通过资源处理链(Resource Chain) 实现: - 资源处理链由一系列 `ResourceResolver`(资源解析器)和 `ResourceTransformer`(资源转换器)按顺序组成; - 其中 `VersionResourceResolver` 是版本控制的核心解析器,负责识别请求中的版本号、剥离版本号后定位真实资源; - Spring 内置了两种开箱即用的版本策略:`FixedVersionStrategy`(固定版本策略)和 `ContentVersionStrategy`(内容哈希版本策略)。 这一版本化机制也是 CVE-2026-41843 路径遍历漏洞的必要触发前提:只有应用显式配置了带版本策略的资源处理链,漏洞才有可能被利用。 1.1 FixedVersionStrategy ------------------------ FixedVersionStrategy可以使用某项属性,或者日期之类的作为版本。 其使用一个全局统一的固定字符串作为版本号,官方内置实现为路径前缀式,版本号统一添加在资源路径的最前端,例如 `/v1.0.0/css/style.css`。 所有资源共用同一个版本号,配置简单,版本号与单个文件内容无关,随应用整体发版同步更新。 下面是具体的demo: 首先通过实现 `WebMvcConfigurer` 配置静态资源映射与版本策略: ```Java @Configuration public class FixedVersionConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/static/**") // 静态资源真实存放位置:classpath 下的 static 目录 .addResourceLocations("classpath:/static/") // 启用资源处理链 .resourceChain(true) .addResolver(new VersionResourceResolver() // 配置固定版本策略,版本号为 v1.0.0,对所有路径生效 .addFixedVersionStrategy("v1.0.0", "/**")); } } ``` 在 `src/main/resources/static/css/` 目录下创建 `style.css`: ```CSS body { margin: 0; padding: 0; color: #333; } ``` 启动应用后,访问带版本前缀的 URL 即可正常获取资源: ```Plain /static/v1.0.0/css/style.css ```  Spring在解析时会自动剥离路径前缀 `v1.0.0`,最终映射到 `classpath:/static/css/style.css`。 1.2 ContentVersionStrategy -------------------------- ContentVersionStrategy会基于每个资源文件的内容计算 MD5 哈希值,将哈希作为版本号嵌入到文件名中。版本号会插入在文件名主体和扩展名之间,例如 `style-2f9a2f4b5c6d.css`; 由于会根据文件内容生成唯一哈希,文件内容不变则哈希不变,缓存持续有效,内容更新则哈希自动变化,触发浏览器缓存刷新。 下面是具体的demo: 首先通过实现 `WebMvcConfigurer` 配置静态资源映射与版本策略: ```Java @Configuration public class ContentVersionConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/res/**") .addResourceLocations("classpath:/static/") .resourceChain(true) .addResolver(new VersionResourceResolver() // 配置内容哈希版本策略,对所有路径生效 .addContentVersionStrategy("/**")); } } ``` 可以定义一个辅助获取版本 URL 的接口,内容哈希由文件内容动态计算,可通过 `ResourceUrlProvider` 获取带版本号的完整资源 URL: ```TypeScript @RestController public class VersionController { @Autowired private ResourceUrlProvider resourceUrlProvider; @GetMapping("/get-version-url") public String getVersionUrl() { // 返回带内容哈希版本的资源完整访问路径 return resourceUrlProvider.getForLookupPath("/res/css/style.css"); } } ``` 同样的在`src/main/resources/static/css/` 目录下创建 `style.css`。 启动应用后,获取对应的版本号,拼接获得对应的URL: ```Plain /res/css/style-7345fe45f851042f3865836ec904d1ce.css ``` 直接访问该 URL 即可正常获取资源;修改文件内容后重新请求,哈希值会自动同步变化。  0x02 漏洞分析与复现 ============ 2.1 分析过程 -------- 以org.springframework:spring-webmvc:6.2.17为例: 基于上面的背景,简单分析下具体静态资源版本的解析过程:  这里实际会调用VersionResourceResolver#resolveResourceInternal方法进行处理:  resolveResourceInternal方法是 Spring 静态资源版本化机制的核心执行入口,职责是识别请求中的版本号、剥离版本号后定位真实资源、并完成版本一致性校验,最终返回包装后的版本化资源对象。 下面逐个进行分析: 首先直接把原始请求路径交给解析链后续的解析器(通常是 `PathResourceResolver`)尝试查找资源,这里是为了兼容不带版本号的普通静态资源请求,如果路径能直接命中资源,直接返回,跳过后续复杂的版本提取、校验流程:  然后是匹配路径对应的版本策略,根据请求路径,匹配预先配置的版本策略(固定版本 / 内容哈希),不同的 URL 模式可以对应不同的策略,若无匹配策略直接返回 null,说明该路径不需要版本化处理:  然后调用对应策略的extractVersion方法,从请求路径中解析出版本号字符串,提取失败说明路径格式不合法、不带版本号:  然后调用对应策略的removeVersion方法,从请求路径中移除版本号,还原出真实的资源查找路径,然后用相关的路径解析真实的资源:  如果相关的资源真实存在,那么计算对应的版本号,和请求携带的候选版本做对比: - 如果一致:将原始资源包装为 `FileNameVersionedResource` 返回,携带版本元数据 - 不一致:判定为非法请求,返回 null  以上便是VersionResourceResolver大致的解析过程。 前面提到了,在解析时,会根据匹配的策略,分别调用对应的extractVersion、getResourceVersion方法以及removeVersion方法进行处理,下面看看FixedVersionStrategy和ContentVersionStrategy的区别: - **FixedVersionStrategy** 可以看到,FixedVersionStrategy的处理是在PrefixVersionPathStrategy中实现的:  其中getResourceVersion方法比较简单,定义的version是什么就返回什么。 另外两个方法可以看到,FixedVersionStrategy主要检查路径开头是否匹配版本前缀,在剔除版本号时,会直接截断对应的版本内容:  按照之前的demo,那是不是意味着如果以如下URL进行访问,即可成功引入路径穿越符: ```Plain /res/v1.0.0../application.properties ``` 但是实际复现时会发现,Spring本身存在一定的拦截机制,并不能完美引入`../` - **ContentVersionStrategy** 可以看到,FixedVersionStrategy的处理是在FileNameVersionPathStrategy中实现的:  其中getResourceVersion方法就是根据文件内容计算MD5 Hash。 查看FileNameVersionPathStrategy具体方法定义,它会在路径中查找所有形如 `-xxx.` 的片段,并把中间的 `xxx` 捕获出来,如果捕获到的内容里还包含横杠,就取最后一个横杠之后的子串作为最终版本号。 而在删除版本号的时,`StringUtils.delete` 的作用是删除字符串中所有出现的目标子串,也就是说会把整条路径里所有 `-版本号` 子串全部无差别删掉:  基于`StringUtils.delete`,那么前面提到的无法完美引入就迎刃而解了,例如下面的例子,在移除版本号后,确实可以得到`../`,但是进一步调试发现,依旧会因为种种限制导致无法利用: ```Plain /res/css/.-7345fe45f851042f3865836ec904d1ce./.-7345fe45f851042f3865836ec904d1ce./application%2eproperties ```  2.2 利用限制 -------- 在1.1节提到了两种策略解析时的利用思路,但是都因为一些限制的原因导致无法利用,下面简单整理下具体的限制。 ### 2.2.1 ResourceHttpRequestHandler SpringMVC主要是利用ResourceHttpRequestHandler来处理静态内容的,它对静态资源的映射提供了默认的配置。 其在解析资源之前,会对请求的path进行合法性检查,这也就解释了为什么没办法直接在路径中引入`../`  ### 2.2.2 版本一致性检查 前面提到,ContentVersionStrategy在移除版本时,会把所有版本号都移除掉,那确实可以规避ResourceHttpRequestHandler的安全检查,但是即使获取到了资源路径,在最后版本一致性检查时,会因为目标与恶意获取的文件名hash不一致,导致无法正常获取文件:  ### 2.2.3 资源读取限制 除此之外,在获取实际路径的资源时,同样存在限制:  这里会调用PathResourceResolver#getResource方法进行处理:   如果资源路径可达,这里会判断对应的资源是否在允许的location下,如果不允许,会返回null:  2.3 漏洞复现 -------- 前面提到了很多利用层面的限制,例如Spring 原生的 `checkResource`方法 兜底确实能拦住标准场景,但生产环境中很多业务为了支持跨目录资源、软链接、自定义存储协议等需求,会自定义 `PathResourceResolver` 并弱化 / 关闭边界校验。这是该漏洞最主要的实战危害场景,也是官方将其定级为中等危害的核心依据。 另外,还可以考虑不同中间件解析差异带来的绕过,这里不进一步展开。 下面简单编写具体的demo进行漏洞复现: 首先自定义后缀式固定版本策略,版本号直接定义为v1,FileNameVersionPathStrategy直接复用AbstractVersionStrategy.PrefixVersionPathStrategy内容: ```Java public class FixedFileNameVersionStrategy extends AbstractVersionStrategy { public FixedFileNameVersionStrategy() { super(new com.atguigu.boot.strategy.FileNameVersionPathStrategy()); } @Override public String getResourceVersion(Resource resource) { return "v1"; } } ``` 然后自定义一个资源解析器,为了方便直接返回true: ```Java public class UnsafePathResourceResolver extends PathResourceResolver { @Override protected boolean checkResource(Resource resource, Resource location) { return true; } } ``` 然后实现 `WebMvcConfigurer` ,指定自定义的静态资源映射与版本策略: ```TypeScript @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 用专属路径/res/**,避免和默认静态资源冲突 registry.addResourceHandler("/res/**") // 静态资源根目录:classpath 下的 static 文件夹 .addResourceLocations("classpath:/static/") // 启用资源处理链 .resourceChain(true) .addResolver(new VersionResourceResolver() .addVersionStrategy(new FixedFileNameVersionStrategy(),"/**")) .addResolver(new UnsafePathResourceResolver()); } } ``` resources的目录结构如下:  这里尝试读取application.properties: ```Plain /res/css/.-v1./.-v1./application%2eproperties ``` 可以看到成功读取对应的文件信息:  如果使用File协议的话,还可以尝试读取/etc/passwd,有兴趣的师傅可以尝试下。 0x03 修复方案 ========= 以修复版本org.springframework:spring-webmvc:7.0.8为例: **主要修复了两处:** - **ResourceHandlerUtils#shouldIgnoreInputPath方法** 在VersionResourceResolver 的资源解析流程中,剥离版本号得到最终路径后,新增了ResourceHandlerUtils#shouldIgnoreInputPath方法调用进行校验:  shouldIgnoreInputPath主要由三个方法组成:  核心方法主要是isInvalidPath方法,里面主要对类似`../`以及一些敏感目录的输入进行了检查:  isInvalidEncodedPath方法主要是考虑到URL编码的情况,解码后继续调用isInvalidPath方法进行判断:  - **FileNameVersionPathStrategy#removeVersion** 修复前,会直接扫描整个请求路径,删除所有出现的 `"-" + version` 子串,出现多少次就删多少次,不限制位置(目录名、文件名中的匹配项都会被删掉):  修复后,找到 `-` + `version` 在路径中最后一次出现的位置,只删掉这一处匹配项,路径前面所有的匹配内容完全保留。只针对路径末端的文件名部分操作,不会触碰前面的目录层级:  可以看到,同样的环境跟poc,在修复后,被应用进行了拦截,无法进一步进行路径遍历漏洞利用:  
发表于 2026-08-04 17:56:48
阅读 ( 50 )
分类:
漏洞分析
0 推荐
收藏
0 条评论
tkswifty
68 篇文章
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!