Sfoglia il codice sorgente

fix 修复 经过加密的请求无法过滤xss问题 将xss实现从gateway移动到common-web解密后过滤

疯狂的狮子Li 1 anno fa
parent
commit
503a0efc31

+ 8 - 0
config/nacos/application-common.yml

@@ -222,6 +222,14 @@ api-decrypt:
   # 对应前端加密公钥 MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==
   privateKey: MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKNPuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gAkM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWowcSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99EcvDQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthhYhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3UP8iWi1Qw0Y=
 
+# 防止XSS攻击
+xss:
+  enabled: true
+  excludeUrls:
+    - /system/notice
+    - /workflow/model/save
+    - /workflow/model/editModelXml
+
 # 接口文档配置
 springdoc:
   api-docs:

+ 0 - 7
config/nacos/ruoyi-gateway.yml

@@ -1,12 +1,5 @@
 # 安全配置
 security:
-  # 防止XSS攻击
-  xss:
-    enabled: true
-    excludeUrls:
-      - /system/notice
-      - /workflow/model/save
-      - /workflow/model/editModelXml
   # 不校验白名单
   ignore:
     whites:

+ 33 - 0
ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/FilterConfig.java

@@ -0,0 +1,33 @@
+package org.dromara.common.web.config;
+
+import jakarta.servlet.DispatcherType;
+import org.dromara.common.web.config.properties.XssProperties;
+import org.dromara.common.web.filter.XssFilter;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+
+/**
+ * Filter配置
+ *
+ * @author Lion Li
+ */
+@AutoConfiguration
+@EnableConfigurationProperties(XssProperties.class)
+public class FilterConfig {
+
+    @Bean
+    @ConditionalOnProperty(value = "xss.enabled", havingValue = "true")
+    public FilterRegistrationBean<XssFilter> xssFilterRegistration() {
+        FilterRegistrationBean<XssFilter> registration = new FilterRegistrationBean<>();
+        registration.setDispatcherTypes(DispatcherType.REQUEST);
+        registration.setFilter(new XssFilter());
+        registration.addUrlPatterns("/*");
+        registration.setName("xssFilter");
+        registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE + 1);
+        return registration;
+    }
+
+}

+ 3 - 5
ruoyi-gateway/src/main/java/org/dromara/gateway/config/properties/XssProperties.java → ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/properties/XssProperties.java

@@ -1,9 +1,8 @@
-package org.dromara.gateway.config.properties;
+package org.dromara.common.web.config.properties;
 
 import lombok.Data;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.cloud.context.config.annotation.RefreshScope;
-import org.springframework.context.annotation.Configuration;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -11,12 +10,11 @@ import java.util.List;
 /**
  * XSS跨站脚本配置
  *
- * @author ruoyi
+ * @author Lion Li
  */
 @Data
-@Configuration
 @RefreshScope
-@ConfigurationProperties(prefix = "security.xss")
+@ConfigurationProperties(prefix = "xss")
 public class XssProperties {
 
     /**

+ 66 - 0
ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/filter/XssFilter.java

@@ -0,0 +1,66 @@
+package org.dromara.common.web.filter;
+
+import jakarta.servlet.*;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.web.config.properties.XssProperties;
+import org.springframework.http.HttpMethod;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 防止XSS攻击的过滤器
+ *
+ * @author ruoyi
+ */
+public class XssFilter implements Filter {
+    /**
+     * 排除链接
+     */
+    public List<String> excludes = new ArrayList<>();
+
+    @Override
+    public void init(FilterConfig filterConfig) throws ServletException {
+        XssProperties properties = SpringUtils.getBean(XssProperties.class);
+        String appName = SpringUtils.getApplicationName();
+        String appPath = "/" + StringUtils.substring(appName, appName.indexOf("-") + 1);
+        List<String> excludeUrls = properties.getExcludeUrls()
+            .stream()
+            .filter(x -> StringUtils.startsWith(x, appPath))
+            .map(x -> x.replaceFirst(appPath, StringUtils.EMPTY))
+            .toList();
+        excludes.addAll(excludeUrls);
+    }
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+        throws IOException, ServletException {
+        HttpServletRequest req = (HttpServletRequest) request;
+        HttpServletResponse resp = (HttpServletResponse) response;
+        if (handleExcludeURL(req, resp)) {
+            chain.doFilter(request, response);
+            return;
+        }
+        XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request);
+        chain.doFilter(xssRequest, response);
+    }
+
+    private boolean handleExcludeURL(HttpServletRequest request, HttpServletResponse response) {
+        String url = request.getServletPath();
+        String method = request.getMethod();
+        // GET DELETE 不过滤
+        if (method == null || HttpMethod.GET.matches(method) || HttpMethod.DELETE.matches(method)) {
+            return true;
+        }
+        return StringUtils.matches(url, excludes);
+    }
+
+    @Override
+    public void destroy() {
+
+    }
+}

+ 97 - 0
ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/filter/XssHttpServletRequestWrapper.java

@@ -0,0 +1,97 @@
+package org.dromara.common.web.filter;
+
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.HtmlUtil;
+import jakarta.servlet.ReadListener;
+import jakarta.servlet.ServletInputStream;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletRequestWrapper;
+import org.dromara.common.core.utils.StringUtils;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * XSS过滤处理
+ *
+ * @author ruoyi
+ */
+public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
+    /**
+     * @param request
+     */
+    public XssHttpServletRequestWrapper(HttpServletRequest request) {
+        super(request);
+    }
+
+    @Override
+    public String[] getParameterValues(String name) {
+        String[] values = super.getParameterValues(name);
+        if (values != null) {
+            int length = values.length;
+            String[] escapseValues = new String[length];
+            for (int i = 0; i < length; i++) {
+                // 防xss攻击和过滤前后空格
+                escapseValues[i] = HtmlUtil.cleanHtmlTag(values[i]).trim();
+            }
+            return escapseValues;
+        }
+        return super.getParameterValues(name);
+    }
+
+    @Override
+    public ServletInputStream getInputStream() throws IOException {
+        // 非json类型,直接返回
+        if (!isJsonRequest()) {
+            return super.getInputStream();
+        }
+
+        // 为空,直接返回
+        String json = StrUtil.str(IoUtil.readBytes(super.getInputStream(), false), StandardCharsets.UTF_8);
+        if (StringUtils.isEmpty(json)) {
+            return super.getInputStream();
+        }
+
+        // xss过滤
+        json = HtmlUtil.cleanHtmlTag(json).trim();
+        byte[] jsonBytes = json.getBytes(StandardCharsets.UTF_8);
+        final ByteArrayInputStream bis = IoUtil.toStream(jsonBytes);
+        return new ServletInputStream() {
+            @Override
+            public boolean isFinished() {
+                return true;
+            }
+
+            @Override
+            public boolean isReady() {
+                return true;
+            }
+
+            @Override
+            public int available() throws IOException {
+                return jsonBytes.length;
+            }
+
+            @Override
+            public void setReadListener(ReadListener readListener) {
+            }
+
+            @Override
+            public int read() throws IOException {
+                return bis.read();
+            }
+        };
+    }
+
+    /**
+     * 是否是Json请求
+     */
+    public boolean isJsonRequest() {
+        String header = super.getHeader(HttpHeaders.CONTENT_TYPE);
+        return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE);
+    }
+}

+ 2 - 1
ruoyi-common/ruoyi-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -1,3 +1,4 @@
+org.dromara.common.web.config.FilterConfig
 org.dromara.common.web.config.I18nConfig
 org.dromara.common.web.config.UndertowConfig
-org.dromara.common.web.config.ResourcesConfig
+org.dromara.common.web.config.ResourcesConfig

+ 0 - 100
ruoyi-gateway/src/main/java/org/dromara/gateway/filter/XssFilter.java

@@ -1,100 +0,0 @@
-package org.dromara.gateway.filter;
-
-import cn.hutool.http.HtmlUtil;
-import org.dromara.common.core.utils.StringUtils;
-import org.dromara.gateway.config.properties.XssProperties;
-import org.dromara.gateway.utils.WebFluxUtils;
-import io.netty.buffer.ByteBufAllocator;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.cloud.gateway.filter.GatewayFilterChain;
-import org.springframework.cloud.gateway.filter.GlobalFilter;
-import org.springframework.core.Ordered;
-import org.springframework.core.io.buffer.*;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpMethod;
-import org.springframework.http.server.reactive.ServerHttpRequest;
-import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
-import org.springframework.stereotype.Component;
-import org.springframework.web.server.ServerWebExchange;
-import reactor.core.publisher.Flux;
-import reactor.core.publisher.Mono;
-
-import java.nio.charset.StandardCharsets;
-
-/**
- * 跨站脚本过滤器
- *
- * @author ruoyi
- */
-@Component
-@ConditionalOnProperty(value = "security.xss.enabled", havingValue = "true")
-public class XssFilter implements GlobalFilter, Ordered {
-    // 跨站脚本的 xss 配置,nacos自行添加
-    @Autowired
-    private XssProperties xss;
-
-    @Override
-    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
-        ServerHttpRequest request = exchange.getRequest();
-        // GET DELETE 不过滤
-        HttpMethod method = request.getMethod();
-        if (method == null || method == HttpMethod.GET || method == HttpMethod.DELETE) {
-            return chain.filter(exchange);
-        }
-        // 非json类型,不过滤
-        if (!WebFluxUtils.isJsonRequest(exchange)) {
-            return chain.filter(exchange);
-        }
-        // excludeUrls 不过滤
-        String url = request.getURI().getPath();
-        if (StringUtils.matches(url, xss.getExcludeUrls())) {
-            return chain.filter(exchange);
-        }
-        ServerHttpRequestDecorator httpRequestDecorator = requestDecorator(exchange);
-        return chain.filter(exchange.mutate().request(httpRequestDecorator).build());
-
-    }
-
-    private ServerHttpRequestDecorator requestDecorator(ServerWebExchange exchange) {
-        ServerHttpRequestDecorator serverHttpRequestDecorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
-            @Override
-            public Flux<DataBuffer> getBody() {
-                Flux<DataBuffer> body = super.getBody();
-                return body.buffer().map(dataBuffers -> {
-                    DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
-                    DataBuffer join = dataBufferFactory.join(dataBuffers);
-                    byte[] content = new byte[join.readableByteCount()];
-                    join.read(content);
-                    DataBufferUtils.release(join);
-                    String bodyStr = new String(content, StandardCharsets.UTF_8);
-                    // 防xss攻击过滤
-                    bodyStr = HtmlUtil.cleanHtmlTag(bodyStr);
-                    // 转成字节
-                    byte[] bytes = bodyStr.getBytes();
-                    NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
-                    DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
-                    buffer.write(bytes);
-                    return buffer;
-                });
-            }
-
-            @Override
-            public HttpHeaders getHeaders() {
-                HttpHeaders httpHeaders = new HttpHeaders();
-                httpHeaders.putAll(super.getHeaders());
-                // 由于修改了请求体的body,导致content-length长度不确定,因此需要删除原先的content-length
-                httpHeaders.remove(HttpHeaders.CONTENT_LENGTH);
-                httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
-                return httpHeaders;
-            }
-
-        };
-        return serverHttpRequestDecorator;
-    }
-
-    @Override
-    public int getOrder() {
-        return Ordered.HIGHEST_PRECEDENCE;
-    }
-}