From 503a0efc319034ccfae77bf820618bce1df8ff7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=96=AF=E7=8B=82=E7=9A=84=E7=8B=AE=E5=AD=90Li?= <15040126243@163.com> Date: Sun, 20 Oct 2024 12:26:05 +0800 Subject: [PATCH] =?UTF-8?q?fix=20=E4=BF=AE=E5=A4=8D=20=E7=BB=8F=E8=BF=87?= =?UTF-8?q?=E5=8A=A0=E5=AF=86=E7=9A=84=E8=AF=B7=E6=B1=82=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E8=BF=87=E6=BB=A4xss=E9=97=AE=E9=A2=98=20=E5=B0=86xss=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E4=BB=8Egateway=E7=A7=BB=E5=8A=A8=E5=88=B0common-web?= =?UTF-8?q?=E8=A7=A3=E5=AF=86=E5=90=8E=E8=BF=87=E6=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/nacos/application-common.yml | 8 ++ config/nacos/ruoyi-gateway.yml | 7 -- .../common/web/config/FilterConfig.java | 33 ++++++ .../web}/config/properties/XssProperties.java | 8 +- .../dromara/common/web/filter/XssFilter.java | 66 ++++++++++++ .../filter/XssHttpServletRequestWrapper.java | 97 +++++++++++++++++ ...ot.autoconfigure.AutoConfiguration.imports | 3 +- .../org/dromara/gateway/filter/XssFilter.java | 100 ------------------ 8 files changed, 209 insertions(+), 113 deletions(-) create mode 100644 ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/FilterConfig.java rename {ruoyi-gateway/src/main/java/org/dromara/gateway => ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web}/config/properties/XssProperties.java (71%) create mode 100644 ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/filter/XssFilter.java create mode 100644 ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/filter/XssHttpServletRequestWrapper.java delete mode 100644 ruoyi-gateway/src/main/java/org/dromara/gateway/filter/XssFilter.java diff --git a/config/nacos/application-common.yml b/config/nacos/application-common.yml index 17b26aec..ee793c7a 100644 --- a/config/nacos/application-common.yml +++ b/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: diff --git a/config/nacos/ruoyi-gateway.yml b/config/nacos/ruoyi-gateway.yml index 60657ae0..ec59aff8 100644 --- a/config/nacos/ruoyi-gateway.yml +++ b/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: diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/FilterConfig.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/FilterConfig.java new file mode 100644 index 00000000..1faf5931 --- /dev/null +++ b/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 xssFilterRegistration() { + FilterRegistrationBean registration = new FilterRegistrationBean<>(); + registration.setDispatcherTypes(DispatcherType.REQUEST); + registration.setFilter(new XssFilter()); + registration.addUrlPatterns("/*"); + registration.setName("xssFilter"); + registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE + 1); + return registration; + } + +} diff --git a/ruoyi-gateway/src/main/java/org/dromara/gateway/config/properties/XssProperties.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/properties/XssProperties.java similarity index 71% rename from ruoyi-gateway/src/main/java/org/dromara/gateway/config/properties/XssProperties.java rename to ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/properties/XssProperties.java index ce77b1ed..369bc4d9 100644 --- a/ruoyi-gateway/src/main/java/org/dromara/gateway/config/properties/XssProperties.java +++ b/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 { /** diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/filter/XssFilter.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/filter/XssFilter.java new file mode 100644 index 00000000..a1bbd296 --- /dev/null +++ b/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 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 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() { + + } +} diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/filter/XssHttpServletRequestWrapper.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/filter/XssHttpServletRequestWrapper.java new file mode 100644 index 00000000..b32b0359 --- /dev/null +++ b/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); + } +} diff --git a/ruoyi-common/ruoyi-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index b0f0fc4d..098c0b71 100644 --- a/ruoyi-common/ruoyi-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/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 \ No newline at end of file +org.dromara.common.web.config.ResourcesConfig diff --git a/ruoyi-gateway/src/main/java/org/dromara/gateway/filter/XssFilter.java b/ruoyi-gateway/src/main/java/org/dromara/gateway/filter/XssFilter.java deleted file mode 100644 index d5016107..00000000 --- a/ruoyi-gateway/src/main/java/org/dromara/gateway/filter/XssFilter.java +++ /dev/null @@ -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 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 getBody() { - Flux 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; - } -}