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
相关产品推荐
相关产品推荐

