Files
Penetration_Testing_POC/books/CVE-2026-41843 Spring Framework路径遍历漏洞浅析.md

17 KiB
Raw Permalink Blame History

CVE-2026-41843 Spring Framework路径遍历漏洞浅析

QIANXIN Team 来源:https://forum.butian.net/share/4953

0x00 CVE-2026-41843

image.png

主要影响范围:

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

image.png

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 即可正常获取资源;修改文件内容后重新请求,哈希值会自动同步变化。

image.png

0x02 漏洞分析与复现

2.1 分析过程

以org.springframework:spring-webmvc:6.2.17为例:

基于上面的背景,简单分析下具体静态资源版本的解析过程:

image.png

这里实际会调用VersionResourceResolver#resolveResourceInternal方法进行处理:

image.png

resolveResourceInternal方法是 Spring 静态资源版本化机制的核心执行入口,职责是识别请求中的版本号、剥离版本号后定位真实资源、并完成版本一致性校验,最终返回包装后的版本化资源对象。

下面逐个进行分析:

首先直接把原始请求路径交给解析链后续的解析器(通常是 PathResourceResolver)尝试查找资源,这里是为了兼容不带版本号的普通静态资源请求,如果路径能直接命中资源,直接返回,跳过后续复杂的版本提取、校验流程:

image.png

然后是匹配路径对应的版本策略,根据请求路径,匹配预先配置的版本策略(固定版本 / 内容哈希),不同的 URL 模式可以对应不同的策略,若无匹配策略直接返回 null,说明该路径不需要版本化处理:

image.png

然后调用对应策略的extractVersion方法,从请求路径中解析出版本号字符串,提取失败说明路径格式不合法、不带版本号:

image.png

然后调用对应策略的removeVersion方法,从请求路径中移除版本号,还原出真实的资源查找路径,然后用相关的路径解析真实的资源:

image.png

如果相关的资源真实存在,那么计算对应的版本号,和请求携带的候选版本做对比:

  • 如果一致:将原始资源包装为 FileNameVersionedResource 返回,携带版本元数据
  • 不一致:判定为非法请求,返回 null

image.png

以上便是VersionResourceResolver大致的解析过程。

前面提到了,在解析时,会根据匹配的策略,分别调用对应的extractVersion、getResourceVersion方法以及removeVersion方法进行处理,下面看看FixedVersionStrategy和ContentVersionStrategy的区别:

  • FixedVersionStrategy

可以看到,FixedVersionStrategy的处理是在PrefixVersionPathStrategy中实现的:

image.png

其中getResourceVersion方法比较简单,定义的version是什么就返回什么。

另外两个方法可以看到,FixedVersionStrategy主要检查路径开头是否匹配版本前缀,在剔除版本号时,会直接截断对应的版本内容:

image.png

按照之前的demo,那是不是意味着如果以如下URL进行访问,即可成功引入路径穿越符:

/res/v1.0.0../application.properties

但是实际复现时会发现,Spring本身存在一定的拦截机制,并不能完美引入../

  • ContentVersionStrategy

可以看到,FixedVersionStrategy的处理是在FileNameVersionPathStrategy中实现的:

image.png

其中getResourceVersion方法就是根据文件内容计算MD5 Hash。

查看FileNameVersionPathStrategy具体方法定义,它会在路径中查找所有形如 -xxx. 的片段,并把中间的 xxx 捕获出来,如果捕获到的内容里还包含横杠,就取最后一个横杠之后的子串作为最终版本号。

而在删除版本号的时,StringUtils.delete 的作用是删除字符串中所有出现的目标子串,也就是说会把整条路径里所有 -版本号 子串全部无差别删掉:

image.png

基于StringUtils.delete,那么前面提到的无法完美引入就迎刃而解了,例如下面的例子,在移除版本号后,确实可以得到../,但是进一步调试发现,依旧会因为种种限制导致无法利用:

/res/css/.-7345fe45f851042f3865836ec904d1ce./.-7345fe45f851042f3865836ec904d1ce./application%2eproperties

image.png

2.2 利用限制

在1.1节提到了两种策略解析时的利用思路,但是都因为一些限制的原因导致无法利用,下面简单整理下具体的限制。

2.2.1 ResourceHttpRequestHandler

SpringMVC主要是利用ResourceHttpRequestHandler来处理静态内容的,它对静态资源的映射提供了默认的配置。

其在解析资源之前,会对请求的path进行合法性检查,这也就解释了为什么没办法直接在路径中引入../

image.png

2.2.2 版本一致性检查

前面提到,ContentVersionStrategy在移除版本时,会把所有版本号都移除掉,那确实可以规避ResourceHttpRequestHandler的安全检查,但是即使获取到了资源路径,在最后版本一致性检查时,会因为目标与恶意获取的文件名hash不一致,导致无法正常获取文件:

image.png

2.2.3 资源读取限制

除此之外,在获取实际路径的资源时,同样存在限制:

image.png

这里会调用PathResourceResolver#getResource方法进行处理:

image.png

image.png

如果资源路径可达,这里会判断对应的资源是否在允许的location下,如果不允许,会返回null:

image.png

2.3 漏洞复现

前面提到了很多利用层面的限制,例如Spring 原生的 checkResource方法 兜底确实能拦住标准场景,但生产环境中很多业务为了支持跨目录资源、软链接、自定义存储协议等需求,会自定义 PathResourceResolver 并弱化 / 关闭边界校验。这是该漏洞最主要的实战危害场景,也是官方将其定级为中等危害的核心依据。

另外,还可以考虑不同中间件解析差异带来的绕过,这里不进一步展开。

下面简单编写具体的demo进行漏洞复现:

首先自定义后缀式固定版本策略,版本号直接定义为v1FileNameVersionPathStrategy直接复用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的目录结构如下:

image.png

这里尝试读取application.properties:

/res/css/.-v1./.-v1./application%2eproperties

可以看到成功读取对应的文件信息:

image.png

如果使用File协议的话,还可以尝试读取/etc/passwd,有兴趣的师傅可以尝试下。

0x03 修复方案

以修复版本org.springframework:spring-webmvc:7.0.8为例:

主要修复了两处:

  • ResourceHandlerUtils#shouldIgnoreInputPath方法

在VersionResourceResolver 的资源解析流程中,剥离版本号得到最终路径后,新增了ResourceHandlerUtils#shouldIgnoreInputPath方法调用进行校验:

image.png

shouldIgnoreInputPath主要由三个方法组成:

image.png

核心方法主要是isInvalidPath方法,里面主要对类似../以及一些敏感目录的输入进行了检查:

image.png

isInvalidEncodedPath方法主要是考虑到URL编码的情况,解码后继续调用isInvalidPath方法进行判断:

image.png

  • FileNameVersionPathStrategy#removeVersion

修复前,会直接扫描整个请求路径,删除所有出现的 "-" + version 子串,出现多少次就删多少次,不限制位置(目录名、文件名中的匹配项都会被删掉):

image.png

修复后,找到 - + version 在路径中最后一次出现的位置,只删掉这一处匹配项,路径前面所有的匹配内容完全保留。只针对路径末端的文件名部分操作,不会触碰前面的目录层级:

image.png

可以看到,同样的环境跟poc,在修复后,被应用进行了拦截,无法进一步进行路径遍历漏洞利用:

image.png

image.png