From 4c225f1467006c4657dbf52748a45b50a314a45b Mon Sep 17 00:00:00 2001 From: zhanglei Date: Mon, 27 Jul 2026 13:40:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20wps=E4=BF=9D=E5=AD=98=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=9B=9E=E8=B0=83=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/XssFilterOverrideConfig.java | 195 ++++++++++++++++++ .../safetyeval/app/executor/WpsExecutor.java | 2 +- 2 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 safety-eval-adapter/src/main/java/org/qinan/safetyeval/adapter/config/XssFilterOverrideConfig.java diff --git a/safety-eval-adapter/src/main/java/org/qinan/safetyeval/adapter/config/XssFilterOverrideConfig.java b/safety-eval-adapter/src/main/java/org/qinan/safetyeval/adapter/config/XssFilterOverrideConfig.java new file mode 100644 index 0000000..7dfcf22 --- /dev/null +++ b/safety-eval-adapter/src/main/java/org/qinan/safetyeval/adapter/config/XssFilterOverrideConfig.java @@ -0,0 +1,195 @@ +package org.qinan.safetyeval.adapter.config; + +import cn.hutool.core.io.IoUtil; +import com.jjb.saas.framework.core.filter.XssFilter; +import com.jjb.saas.framework.core.filter.XssHttpRequestWrapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ReadListener; +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * 框架 XssFilter / LoggingBodyFilter 会按文本处理 body,破坏 WPS 二进制上传。 + *

+ * /safetyEval-h5/**:用可重复读的 byte[] 包装请求(不改 Interceptor 类型,避免启动失败); + * 其它接口仍走原 XSS / LoggingBodyFilter。 + */ +@Slf4j +@Configuration +public class XssFilterOverrideConfig { + + private static final String H5_URI_MARKER = "/safetyEval-h5/"; + private static final String FRAMEWORK_XSS_BEAN_NAME = "xssFilterFilterRegistrationBean"; + private static final String LOGGING_BODY_FILTER_BEAN_NAME = "loggingBodyFilter"; + private static final String LOGGING_BODY_FILTER_CLASS = + "org.minbox.framework.logging.client.filter.LoggingBodyFilter"; + + @Bean + public static BeanPostProcessor disableBinaryCorruptingFilters() { + return new BeanPostProcessor() { + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof FilterRegistrationBean) { + return processFilterRegistration((FilterRegistrationBean) bean, beanName); + } + + if (bean instanceof Filter + && (LOGGING_BODY_FILTER_BEAN_NAME.equals(beanName) + || LOGGING_BODY_FILTER_CLASS.equals(bean.getClass().getName()))) { + Filter original = (Filter) bean; + log.info("wrap LoggingBodyFilter beanName={}, class={}", beanName, bean.getClass().getName()); + return new OncePerRequestFilter() { + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain chain) throws ServletException, IOException { + String uri = request.getRequestURI(); + if (isH5Uri(uri)) { + byte[] body = IoUtil.readBytes(request.getInputStream()); + log.info("h5 binary body cached, uri={}, size={}", uri, body.length); + chain.doFilter(new RepeatableByteRequestWrapper(request, body), response); + return; + } + original.doFilter(request, response, chain); + } + }; + } + + return bean; + } + + private Object processFilterRegistration(FilterRegistrationBean registration, String beanName) { + if (beanName.startsWith("safety") + || "safetyXssFilterRegistration".equals(beanName)) { + return registration; + } + Filter filter = null; + try { + filter = registration.getFilter(); + } catch (Exception ignored) { + // ignore uninitialized registration + } + boolean match = FRAMEWORK_XSS_BEAN_NAME.equals(beanName) || filter instanceof XssFilter; + if (match) { + registration.setEnabled(false); + log.info("disabled framework XSS filter registration: beanName={}", beanName); + } + return registration; + } + }; + } + + @Bean + public FilterRegistrationBean safetyXssFilterRegistration() { + FilterRegistrationBean bean = new FilterRegistrationBean<>(); + bean.setFilter(new OncePerRequestFilter() { + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain chain) throws ServletException, IOException { + String uri = request.getRequestURI(); + if (isH5Uri(uri)) { + log.info("xss bypass for h5 uri={}", uri); + chain.doFilter(request, response); + return; + } + chain.doFilter(new XssHttpRequestWrapper(request), response); + } + }); + bean.setName("safetyXssFilter"); + bean.addUrlPatterns("/*"); + bean.setOrder(Ordered.HIGHEST_PRECEDENCE + 20); + return bean; + } + + private static boolean isH5Uri(String uri) { + return uri != null && uri.contains(H5_URI_MARKER); + } + + /** + * 每次 getInputStream/getReader 都从同一份 byte[] 新建流, + * 避免 LoggingWebInterceptor 里 new RequestWrapper 关闭流后 Controller 读不到。 + */ + static final class RepeatableByteRequestWrapper extends HttpServletRequestWrapper { + + private final byte[] body; + + RepeatableByteRequestWrapper(HttpServletRequest request, byte[] body) { + super(request); + this.body = body == null ? new byte[0] : body; + } + + @Override + public ServletInputStream getInputStream() { + ByteArrayInputStream inputStream = new ByteArrayInputStream(body); + return new ServletInputStream() { + @Override + public boolean isFinished() { + return inputStream.available() == 0; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) { + // not used + } + + @Override + public int read() { + return inputStream.read(); + } + }; + } + + @Override + public BufferedReader getReader() { + Charset charset = resolveCharset(); + return new BufferedReader(new InputStreamReader(getInputStream(), charset)); + } + + @Override + public int getContentLength() { + return body.length; + } + + @Override + public long getContentLengthLong() { + return body.length; + } + + private Charset resolveCharset() { + String encoding = getCharacterEncoding(); + if (encoding == null || encoding.isEmpty()) { + return StandardCharsets.UTF_8; + } + try { + return Charset.forName(encoding); + } catch (Exception e) { + return StandardCharsets.UTF_8; + } + } + } +} diff --git a/safety-eval-app/src/main/java/org/qinan/safetyeval/app/executor/WpsExecutor.java b/safety-eval-app/src/main/java/org/qinan/safetyeval/app/executor/WpsExecutor.java index 59d1897..a19b21a 100644 --- a/safety-eval-app/src/main/java/org/qinan/safetyeval/app/executor/WpsExecutor.java +++ b/safety-eval-app/src/main/java/org/qinan/safetyeval/app/executor/WpsExecutor.java @@ -172,7 +172,7 @@ public class WpsExecutor implements WpsApi { public Map uploadBinary(String fileId, InputStream inputStream) { try { wpsDocDomainService.getByFileId(fileId); - FileInfo ossFileInfo = fileStorageService.of(inputStream, fileId).upload(); + FileInfo ossFileInfo = fileStorageService.of(inputStream, fileId).setSaveFilename(UUID.randomUUID().toString().replace("-", "")+".docx").upload(); FileStorage fileStorage = FileStorageHelper.buildResource(ossFileInfo, fileId, true); FileStorage saved = fileStorageDomainService.saveResource(fileStorage); wpsDocDomainService.savePendingUpload(fileId, saved.getId(), saved.getUrl(), saved.getSize());