17 KiB
CVE-2026-41843 Spring Framework路径遍历漏洞浅析
QIANXIN Team 来源:https://forum.butian.net/share/4953
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 配置静态资源映射与版本策略:
@Configuration
public class FixedVersionConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/")
.resourceChain(true)
.addResolver(new VersionResourceResolver()
.addFixedVersionStrategy("v1.0.0", "/**"));
}
}
在 src/main/resources/static/css/ 目录下创建 style.css:
body {
margin: 0;
padding: 0;
color: #333;
}
启动应用后,访问带版本前缀的 URL 即可正常获取资源:
/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 配置静态资源映射与版本策略:
@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:
@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:
/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进行访问,即可成功引入路径穿越符:
/res/v1.0.0../application.properties
但是实际复现时会发现,Spring本身存在一定的拦截机制,并不能完美引入../
- ContentVersionStrategy
可以看到,FixedVersionStrategy的处理是在FileNameVersionPathStrategy中实现的:
其中getResourceVersion方法就是根据文件内容计算MD5 Hash。
查看FileNameVersionPathStrategy具体方法定义,它会在路径中查找所有形如 -xxx. 的片段,并把中间的 xxx 捕获出来,如果捕获到的内容里还包含横杠,就取最后一个横杠之后的子串作为最终版本号。
而在删除版本号的时,StringUtils.delete 的作用是删除字符串中所有出现的目标子串,也就是说会把整条路径里所有 -版本号 子串全部无差别删掉:
基于StringUtils.delete,那么前面提到的无法完美引入就迎刃而解了,例如下面的例子,在移除版本号后,确实可以得到../,但是进一步调试发现,依旧会因为种种限制导致无法利用:
/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内容:
public class FixedFileNameVersionStrategy extends AbstractVersionStrategy {
public FixedFileNameVersionStrategy() {
super(new com.atguigu.boot.strategy.FileNameVersionPathStrategy());
}
@Override
public String getResourceVersion(Resource resource) {
return "v1";
}
}
然后自定义一个资源解析器,为了方便直接返回true:
public class UnsafePathResourceResolver extends PathResourceResolver {
@Override
protected boolean checkResource(Resource resource, Resource location) {
return true;
}
}
然后实现 WebMvcConfigurer ,指定自定义的静态资源映射与版本策略:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/res/**")
.addResourceLocations("classpath:/static/")
.resourceChain(true)
.addResolver(new VersionResourceResolver()
.addVersionStrategy(new FixedFileNameVersionStrategy(),"/**"))
.addResolver(new UnsafePathResourceResolver());
}
}
resources的目录结构如下:
这里尝试读取application.properties:
/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,在修复后,被应用进行了拦截,无法进一步进行路径遍历漏洞利用:






























