You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot中multipart/form-data请求的XSS Filter实现方案

问题

已成功实现针对application/json类型请求的XSS Filter且运行正常,但在实现multipart/form-data类型请求的过滤时遇到困难。请问需要编写单独的Filter,还是可以在现有Filter中处理?恳请告知正确的实现方式。

现有实现代码

XSSFilter.java

import java.io.IOException;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;

@Component
public class XSSFilter implements Filter {

  @Override
  public void doFilter(
      ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
      throws IOException, ServletException {
    filterChain.doFilter(
        new XSSRequestWrapper((HttpServletRequest) servletRequest), servletResponse);
  }
}

XSSRequestWrapper.java

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.safety.Safelist;

public class XSSRequestWrapper extends HttpServletRequestWrapper {
  private final String requestJsonString;
  BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));

  public XSSRequestWrapper(HttpServletRequest httpServletRequest) {
    super(httpServletRequest);

    try {
      StringBuilder stringBuilder = new StringBuilder();
      bufferedReader = httpServletRequest.getReader();
      char[] charBuffer = new char[128];
      int bytesRead;
      while ((bytesRead = bufferedReader.read(charBuffer)) != -1) {
        stringBuilder.append(charBuffer, 0, bytesRead);
      }
      requestJsonString = stringBuilder.toString();
      JsonNode jsonNode = new ObjectMapper().readTree(requestJsonString);
      processJsonNode(jsonNode);
    } catch (IOException e) {
      throw new RuntimeException(e);
    } finally {
      try {
        bufferedReader.close();
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }
  }

  @Override
  public ServletInputStream getInputStream() {
    return new ServletInputStreamWrapper(requestJsonString.getBytes());
  }

  @Override
  public BufferedReader getReader() {
    return new BufferedReader(new InputStreamReader(this.getInputStream()));
  }

  private void processJsonNode(JsonNode jsonNode) {
    if (jsonNode.isObject()) {
      jsonNode.fields().forEachRemaining(entry -> processJsonNode(entry.getValue()));
    } else if (jsonNode.isArray()) {
      jsonNode.elements().forEachRemaining(this::processJsonNode);
    } else {
      sanitize("key", jsonNode.asText());
    }
  }

  @Override
  public Enumeration<String> getHeaderNames() {
    Enumeration<String> headerNames = super.getHeaderNames();
    Collections.list(headerNames)
        .forEach(headerName -> sanitize(headerName, getHeader(headerName)));
    return headerNames;
  }

  @Override
  public String[] getParameterValues(String parameter) {
    String[] parameterValues = super.getParameterValues(parameter);
    Arrays.stream(parameterValues).forEach(value -> sanitize(parameter, value));
    return parameterValues;
  }

  public void sanitize(String key, String value) {
    Safelist safelist = Safelist.none();
    Document.OutputSettings outputSettings = new Document.OutputSettings().prettyPrint(false);
    if (!value.equalsIgnoreCase(Jsoup.clean(value, "", safelist, outputSettings))) {
      throw new SecurityException(
          "XSS attack error, by the key '" + key + "' and the value is '" + value + "'");
    }
  }
}

ServletInputStreamWrapper.java

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import java.io.IOException;

public class ServletInputStreamWrapper extends ServletInputStream {

  private final byte[] data;
  private int idx = 0;

  public ServletInputStreamWrapper(byte[] data) {
    this.data = data;
  }

  @Override
  public int read() throws IOException {
    if (idx >= data.length) {
      return -1;
    }
    return data[idx++];
  }

  @Override
  public boolean isFinished() {
    return false;
  }

  @Override
  public boolean isReady() {
    return false;
  }

  @Override
  public void setReadListener(ReadListener readListener) {}
}
解决方案

不需要编写单独的Filter,可以在现有Filter中扩展处理逻辑,核心思路是根据请求的Content-Type分支处理不同类型的请求,具体实现步骤如下:

1. 调整Filter执行顺序

由于multipart/form-data请求需要先被Spring的MultipartFilter解析,才能获取到表单参数,因此需要确保你的XSSFilter在MultipartFilter之后执行。在XSSFilter类上添加@Order注解:

import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;

@Component
@Order(Ordered.LOWEST_PRECEDENCE - 10) // 确保在MultipartFilter之后运行
public class XSSFilter implements Filter {
    // 原有代码不变
}

2. 修改XSSRequestWrapper,支持multipart请求处理

修改构造方法,根据Content-Type判断请求类型,分别处理JSON、multipart及普通表单请求:

import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;

public class XSSRequestWrapper extends HttpServletRequestWrapper {
    private final String requestJsonString;
    // 移除多余的System.in初始化,避免错误
    private BufferedReader bufferedReader;

    public XSSRequestWrapper(HttpServletRequest httpServletRequest) {
        super(httpServletRequest);
        String contentType = httpServletRequest.getContentType();

        try {
            if (contentType != null && contentType.startsWith("multipart/form-data")) {
                // 处理multipart/form-data请求
                processMultipartRequest((MultipartHttpServletRequest) httpServletRequest);
                requestJsonString = null; // multipart请求不需要保存JSON字符串
            } else if (contentType != null && contentType.startsWith("application/json")) {
                // 原有JSON请求处理逻辑
                StringBuilder stringBuilder = new StringBuilder();
                bufferedReader = httpServletRequest.getReader();
                char[] charBuffer = new char[128];
                int bytesRead;
                while ((bytesRead = bufferedReader.read(charBuffer)) != -1) {
                    stringBuilder.append(charBuffer, 0, bytesRead);
                }
                requestJsonString = stringBuilder.toString();
                JsonNode jsonNode = new ObjectMapper().readTree(requestJsonString);
                processJsonNode(jsonNode);
            } else {
                // 处理其他类型请求,如application/x-www-form-urlencoded
                processRegularParameters(httpServletRequest);
                requestJsonString = null;
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    // 处理multipart请求的表单参数和文件名
    private void processMultipartRequest(MultipartHttpServletRequest multipartRequest) {
        // 检查所有表单参数
        multipartRequest.getParameterMap().forEach((key, values) -> {
            Arrays.stream(values).forEach(value -> sanitize(key, value));
        });
        // 检查上传文件的文件名(可选,根据业务需要)
        for (MultipartFile file : multipartRequest.getFiles()) {
            if (file.getOriginalFilename() != null) {
                sanitize("upload-filename", file.getOriginalFilename());
            }
        }
    }

    // 处理普通表单参数请求
    private void processRegularParameters(HttpServletRequest request) {
        request.getParameterMap().forEach((key, values) -> {
            Arrays.stream(values).forEach(value -> sanitize(key, value));
        });
    }

    // 原有方法不变:getInputStream、getReader、processJsonNode、getHeaderNames、getParameterValues、sanitize
}

3. 关键注意事项

  • 避免重复读取流:multipart请求解析后,流已经被Spring处理过,不能再调用getReader()或getInputStream(),因此在multipart分支直接解析参数即可。
  • 文件内容处理:如果需要检查上传文件的内容(如文本文件),可以在processMultipartRequest中读取MultipartFile的输入流进行XSS检查,但二进制文件无需处理。
  • Spring Multipart配置:确保Spring已启用multipart支持,可在application.properties中添加:
    spring.servlet.multipart.enabled=true
    spring.servlet.multipart.max-file-size=10MB
    spring.servlet.multipart.max-request-size=10MB
    

总结

  • 复用现有XSSFilter,通过Content-Type分支处理JSON、multipart及普通表单请求
  • 借助Spring的MultipartHttpServletRequest简化multipart请求的参数解析
  • 调整Filter顺序,确保在multipart解析完成后执行XSS检查
  • 针对不同请求类型的参数(JSON节点、表单字段、文件名)分别执行XSS校验

内容的提问来源于stack exchange,提问作者kunal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 16:25:55