浅谈Spring WebFlux文件上传解析

在 Spring WebFlux 中,可以使用 org.springframework.http.codec.multipart.FilePart 来处理文件上传。浅谈其具体实现。

0x00 前言

在 Spring MVC 中,可以使用 org.springframework.web.multipart.MultipartFile 类来处理文件上传,将multipart中的对象封装到MultipartRequest对象中然后进行相应的处理:

@RequestMapping(value={"/uploadFile"},method={RequestMethod.POST})
public String uploadFile(MultipartFile file,String type,HttpServletResponse response) throws Exception{
        String UPLOADED_FOLDER="/resource/upload/";
        if(!file.isEmpty()){
                String path = UPLOADED_FOLDER + file.getOriginalFilename();
                File targetFile = new File(path);
                FileUtils.inputStreamToFile(file.getInputStream(),targetFile);
                        ......
                        ......

        }
}

而在 Spring WebFlux 中,不能直接使用 org.springframework.web.multipart.MultipartFile 类来处理文件上传,因为它是为 Spring MVC 提供的传统 Servlet 基础的文件上传功能。而Spring WebFlux是基于 Reactor 的。

在 Spring WebFlux 中,可以使用 org.springframework.http.codec.multipart.FilePart 来处理文件上传。FilePart 是 Spring WebFlux 提供的用于表示上传文件的类,例如如下的例子:

@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Mono upload(
        @RequestPart("file") final FilePart filePart,
        final FormData formData
) {
    log.debug("formData => {}", formData);
    System.out.println(filePart.filename());

    final File directory = new File(UPLOAD_DIRECTORY);
    if(!directory.exists()){
        directory.mkdirs();
    }

    final File file = new File(directory, filePart.filename());

    return filePart
            .transferTo(file)
            .then(Mono.fromCallable(() -> {
                final Map map = new HashMap<>();
                map.put("name", file.getName());
                map.put("lastModified", file.lastModified());
                map.put("size", file.length());
                return map;
            }));
}

DefaultServerWebExchange 是 Spring WebFlux 框架中的一个类,它实现了 ServerWebExchange 接口,用于表示一个服务器和客户端之间的交互。其封装了底层的 HTTP 请求和响应,提供了访问请求和响应信息的方法,如获取请求方法、路径、请求头、请求体等,以及设置响应状态码、响应头、响应体等。

其中的initMultipartData 方法的用户初始化处理 multipart 请求时的相关数据,以spring-web-5.3.27为例,查看其解析过程:

image.png

0x01 判断是否是Multipart请求

1.1 判断方式

initMultipartData 方法中,首先会判断当前请求是否是Multipart上传请求,首先会从获取当前请求header的ContentType,然后调用org.springframework.http.MediaType#isCompatibleWith方法进行判断:

image.png

实际上调用的是其父类MimeType的方法:

image.png

首先如果传入的 other 参数为 null,则返回 false。如果两个 MIME 类型的类型和子类型都不是通配符类型(即非 *),会比较两者是否类型相同且子类型相同,是的话则返回true:

image.png

例如在判断文件上传请求时,会判断是否是匹配multipart/form-data:

image.png

如果一个 MIME 类型的子类型是通配符类型 *,则返回true:

image.png

也就是说,当Content-Type为multipart/*或者multipart/form-data都会认为是Multipart请求。(跟Spring MVC一样支持大小写转换)。

当然了也支持通过配置consumes = MediaType.MULTIPART_FORM_DATA_VALUE,限制接受的内容类型为multipart/form-data。仅当请求的Content-Type为multipart/form-data时,该方法才会被调用:

@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Mono upload(@RequestPart("file") final FilePart filePart,
        final FormData formData
) {

    ......
}

1.2 与其他框架的区别

  • Struts2
if (content_type != null && content_type.contains("multipart/form-data")){
......
}
  • Spring MVC

主要有两个解析器:

  1. StandardServletMultipartResolver解析器:

通过判断请求的Content-Type来判断是否是文件请求:

public boolean isMultipart(HttpServletRequest request) {  
   return StringUtils.startsWithIgnoreCase(request.getContentType(),  
         (this.strictServletCompliance ? "multipart/form-data" : "multipart/"));  
}

其中,strictServletCompliance是StandardServletMultipartResolver的成员变量,默认false,表示是否严格遵守Servlet 3.0规范。简单来说就是对Content-Type校验的严格程度。如果strictServletCompliance为false,请求头以multipart/开头就满足文件请求条件;如果strictServletCompliance为true,则需要请求头以multipart/form-data开头。

  1. CommonsMultipartResolver解析器:

CommonsMultipartResolver解析器会根据请求方法和请求头来判断文件请求:

public boolean isMultipart(HttpServletRequest request) {  
   return (this.supportedMethods != null ?  
         this.supportedMethods.contains(request.getMethod()) &&  
               FileUploadBase.isMultipartContent(new ServletRequestContext(request)) :  
         ServletFileUpload.isMultipartContent(request));  
}

supportedMethods成员变量表示支持的请求方法,默认为null,可以在初始化时指定。主要判断的方法在isMultipartContent(),请求头Content-Type为以multipart/开头即会认为是Multipart请求:

image.png

0x02 上传解析请求过程

在Spring WebFlux中,SynchronossPartHttpMessageReaderDefaultPartHttpMessageReader 都是用于处理和解析 HTTP multipart 请求的组件。

默认情况下,Spring WebFlux使用DefaultPartHttpMessageReader

也可以使用 SynchronossPartHttpMessageReader,它是基于 Synchronoss NIO Multipart 库的。两者都可以通过 ServerCodecConfigurer Bean进行配置:

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
        SynchronossPartHttpMessageReader reader = new SynchronossPartHttpMessageReader();
        reader.setMaxParts(1);
        reader.setMaxDiskUsagePerPart(10L * 1024L);
        reader.setEnableLoggingRequestDetails(true);

        MultipartHttpMessageReader multipartReader = new MultipartHttpMessageReader(reader);
        multipartReader.setEnableLoggingRequestDetails(true);
        configurer.defaultCodecs().multipartReader(multipartReader);
    }

}

下面查看具体的解析方式。

2.1 DefaultPartHttpMessageReader解析

在解析每一个part的时候,会根据header调用org.springframework.http.codec.multipart.PartGenerator#newPart进行处理:

image.png

这里会判断是否是formFiled:

image.png

这里会判断上传的filename参数是否为空,而filename参数是通过org.springframework.http.ContentDisposition#parse方法解析的,这里对相关的http内容进行了处理跟封装:

image.png

查看具体的处理过程,实际上跟Spring MVC中使用StandardMultipartHttpServletRequest解析器解析是类似的:

image.png

如果传入的multipart请求无法直接使用filename=解析出文件名,会判断filename是否以=?开头,是的话会进入BASE64_ENCODED_PATTERN的正则匹配中,大致的可以知道需要匹配的内容应该是=?编码方式?B?编码内容?=

image.png

image.png

对于filename*=的内容,例如传入的UTF-8'1.jpg'1.jsp会被解析成UTF-8编码,最终的文件名为1.jsp,而1.jpg则会被丢弃(获取到filename*=后的内容后,首先切割第一个',通过Charset获取对应的编码方式,然后再切割第二个' 后的内容,并根据前面的编码方式进行解码操作,最后返回对应的filename。可以看到实际上两个'之间是可以任意填充内容的(单引号之间的内容在实际解析时会被忽略掉)):

image.png

image.png

如果不满足上述条件的话,则直接将value赋值给filename,实际上就是获取"间的内容:

String value = part.startsWith("\"", eqIndex + 1) && part.endsWith("\"") ? part.substring(eqIndex + 2, part.length() - 1) : part.substring(eqIndex + 1);

最后会将分析的结果封装成ContentDisposition对象进行返回:

image.png

2.2 SynchronossPartHttpMessageReader解析

该解析器在使用时除了需要额外配置以外,还需要引入对应的依赖:

image.png

在解析每一个part的时候,会调用org.springframework.http.codec.multipart.SynchronossPartHttpMessageReader中FluxSinkAdapterListener的createPart方法进行处理:

image.png

实际上调用的是org.synchronoss.cloud.nio.multipart.MultipartUtils#getFileName对请求的headers进行处理:

image.png

在getFileName方法中,首先会调用getHeader方法获取contentDisposition:

image.png

这里会先将headerName转换成小写(multipart请求的话,headerName就是Content-disposition),然后再获取对应的值进行返回:

image.png

获取到contentDisposition后,首先将其转换成小写,若其是以form-data或者attachment开头的话,调用org.synchronoss.cloud.nio.multipart.util.ParameterParser解析器进行处理:

image.png

查看org.synchronoss.cloud.nio.multipart.util.ParameterParser解析器的核心方法parse,这里主要是将contentDisposition转换成char数组进行处理:

image.png

这里会根据=划分对应的paramName和paramValue,如果当前字符是等号 (=),则提取参数值,同时还会对参数值进行解码操作:

image.png

查看解码操作org.synchronoss.cloud.nio.multipart.util.MimeUtility#decodeText的具体实现,可以看到这里实际上是对=?开头的内容进行解析:

image.png

在ParameterParser解析器解析完成后,如果解析的param中包含fileName,会进行trim操作,删除字符串开头和结尾的空白字符:

image.png

最后如果返回fileName不为null,则会创建SynchronossFilePart对象,从而进一步被处理或者传递给其他组件。例如保存文件、处理表单数据等。

2.3 两者的区分

  • SynchronossPartHttpMessageReader不支持使用filename*=进行文件名解析:

image.png

  • SynchronossPartHttpMessageReader会对解析的filename进行trim操作,删除字符串开头和结尾的空白字符,而DefaultPartHttpMessageReader不会:

image.png

image.png

  • 获取filename的位置不一样

DefaultPartHttpMessageReader在解析时会有这么一个判断条件,当当前解析的参数为filename且值为null的时候才会进行解析,第一次解析时已经获取到对应的值了,所以后续的值不会进行解析,也就是说当存在多个filename参数的时候,其会获取第一个的值:

image.png

image.png

SynchronossPartHttpMessageReader在解析时会通过HashMap的形式来存储解析到的param以及对应的值,那么当第二次获取到filename参数时,会覆盖掉原来的值,所以当存在多个filename参数的时候,其会获取最后一个的值:

image.png

image.png

  • =?开头的内容解析的差异

DefaultPartHttpMessageReader在解析时是通过正则进行匹配的,并且需要value以=?开头:

image.png

image.png

SynchronossPartHttpMessageReader在解析时若当前字符为一个空白字符(空格、制表符、回车或换行)时,记录空白数字的起始和结束位置并进行处理:

image.png

image.png

0x03 获取上传的文件名

Spring WebFlux在处理multipart请求时,如果请求中包含文件上传的部分(Part),可以使用filePart.filename()方法来获取该部分对应的文件名。该方法返回一个字符串,表示文件的原始文件名。

通常,开发人员可以使用该方法来获取上传文件的文件名,并进行相应的处理,例如文件存储、文件名校验等操作。

当使用DefaultPartHttpMessageReader解析进行解析时,实际上是从ContentDispositio对象中获取的:

image.png

使用SynchronossPartHttpMessageReader解析时,是从SynchronossFilePart对象的filename属性进行获取的:

image.png

根据前面的分析,两者在整个过程没有对类似../的路径进行检查&过滤,由于获取的fileName未进行安全处理,在使用File创建文件时,若路径处path写入../../穿越符号,是可以跨目录新建文件的。

那么在实际利用时,可以尝试通过linux写入定时任务(etc/cron.d/下的文件可以以任意后缀命名,如果未对filename进行重命名的话还可以绕过上传的后缀限制)、ssh公钥(需要满足root权限),甚至是替换 JDK HOME 目录下的系统 jar 文件,再主动触发 jar 文件里的类初始化来达到执行任意代码的效果。

0x04 SpringMVC的Multipart请求绕过

众所周知,在SpringMVC中,当接收到一个multipart请求后,会调用 MultipartResolver 的 resolveMultipart() 方法对请求的数据进行解析并将解析结果封装到中HttpServletRequest。很多时候一些安全检测的filter在进行类似SQL注入、XSS的过滤时没有考虑到上述情况,那么就可以尝试将普通的GET/POST转换成Multipart请求,绕过对应的安全检查。

以如下Controller为例,查看Spring WebFlux是否也存在类似的绕过场景:

@RequestMapping("/manage")
public String manage(@RequestParam String param) {
    return "param:"+param;
}

正常情况下,通过GET请求访问该资源可以正常获取param参数的内容:

image.png

当使用POST方法请求时,会返回400 Status:

image.png

查阅官方文档https://docs.spring.io/spring-framework/reference/web/webflux/controller/ann-methods/requestparam.html 可知:

在Spring webflux中,@RequestParam注解仅支持url传参方式,无法处理form-data和multipart的方法。如果想处理类似的请求,可以使用ServerWebExchange进行处理。

The Servlet API “request parameter” concept conflates query parameters, form data, and multiparts into one. However, in WebFlux, each is accessed individually through ServerWebExchange. While @RequestParam binds to query parameters only, you can use data binding to apply query parameters, form data, and multiparts to a command object.

例如需要通过form-data的方式获取param参数:

@PostMapping("/manage")
public Mono manage(ServerWebExchange exchange) {
    return exchange.getFormData()
            .flatMap(formData -> {
                String paramName1 = formData.getFirst("param"); // 获取 POST 参数 "param"
                // 处理参数并返回结果
                return Mono.just("Param: " + paramName1);
            });
}

image.png

同样的,上述Controller代码并不能处理multipart的请求:

image.png

如果需要获取multipart的数据,需要额外的调用ServerWebExchange方法进行处理:

@PostMapping(value = "/handleMultipartRequest", consumes = "multipart/form-data")
public Mono handleMultipartRequest(ServerWebExchange exchange) {
    return exchange.getMultipartData()
            .flatMap(parts -> {
                Part part = parts.getFirst("param");
                if (part instanceof FormFieldPart){
                    return Mono.just("param: " + ((FormFieldPart) part).value());
                }

                // 处理参数并返回结果
                return Mono.just("param: " + null);
            });
}

image.png

也就是说,默认情况下,Spring WebFlux并不像Spring MVC一样,能在多个请求方式之间灵活解析转换。

  • 发表于 2023-06-26 09:00:02
  • 阅读 ( 8433 )
  • 分类:代码审计

0 条评论

请先 登录 后评论
tkswifty
tkswifty

64 篇文章

站长统计