From c42b165f2718e1f5fdd70e7e825e5d93dc847f5a Mon Sep 17 00:00:00 2001 From: smallchill Date: Tue, 1 Mar 2022 23:49:39 +0800 Subject: [PATCH] =?UTF-8?q?:zap:=20=E4=BC=98=E5=8C=96Xss=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...uration.java => RequestConfiguration.java} | 28 ++--- .../BladeHttpServletRequestWrapper.java | 119 ++++++++++++++++++ .../BladeRequestFilter.java} | 29 +++-- .../RequestProperties.java} | 18 ++- .../XssHtmlFilter.java} | 18 +-- .../XssHttpServletRequestWrapper.java | 58 +++------ .../xss => request}/XssProperties.java | 2 +- .../springblade/core/tool/utils/WebUtil.java | 75 +++++++++++ 8 files changed, 267 insertions(+), 80 deletions(-) rename blade-core-tool/src/main/java/org/springblade/core/tool/config/{XssConfiguration.java => RequestConfiguration.java} (62%) create mode 100644 blade-core-tool/src/main/java/org/springblade/core/tool/request/BladeHttpServletRequestWrapper.java rename blade-core-tool/src/main/java/org/springblade/core/tool/{support/xss/XssFilter.java => request/BladeRequestFilter.java} (63%) rename blade-core-tool/src/main/java/org/springblade/core/tool/{support/xss/XssUrlProperties.java => request/RequestProperties.java} (74%) rename blade-core-tool/src/main/java/org/springblade/core/tool/{support/xss/HtmlFilter.java => request/XssHtmlFilter.java} (98%) rename blade-core-tool/src/main/java/org/springblade/core/tool/{support/xss => request}/XssHttpServletRequestWrapper.java (77%) rename blade-core-tool/src/main/java/org/springblade/core/tool/{support/xss => request}/XssProperties.java (95%) diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/config/XssConfiguration.java b/blade-core-tool/src/main/java/org/springblade/core/tool/config/RequestConfiguration.java similarity index 62% rename from blade-core-tool/src/main/java/org/springblade/core/tool/config/XssConfiguration.java rename to blade-core-tool/src/main/java/org/springblade/core/tool/config/RequestConfiguration.java index 89c159e..2dd4f6d 100644 --- a/blade-core-tool/src/main/java/org/springblade/core/tool/config/XssConfiguration.java +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/config/RequestConfiguration.java @@ -16,10 +16,9 @@ package org.springblade.core.tool.config; import lombok.AllArgsConstructor; -import org.springblade.core.tool.support.xss.XssFilter; -import org.springblade.core.tool.support.xss.XssProperties; -import org.springblade.core.tool.support.xss.XssUrlProperties; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springblade.core.tool.request.BladeRequestFilter; +import org.springblade.core.tool.request.RequestProperties; +import org.springblade.core.tool.request.XssProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; @@ -29,31 +28,28 @@ import org.springframework.core.Ordered; import javax.servlet.DispatcherType; /** - * Xss配置类 + * 过滤器配置类 * * @author Chill */ @Configuration(proxyBeanMethods = false) @AllArgsConstructor -@ConditionalOnProperty(value = "blade.xss.enabled", havingValue = "true") -@EnableConfigurationProperties({XssProperties.class, XssUrlProperties.class}) -public class XssConfiguration { +@EnableConfigurationProperties({RequestProperties.class, XssProperties.class}) +public class RequestConfiguration { + private final RequestProperties requestProperties; private final XssProperties xssProperties; - private final XssUrlProperties xssUrlProperties; /** - * 防XSS注入 - * - * @return FilterRegistrationBean + * 全局过滤器 */ @Bean - public FilterRegistrationBean xssFilterRegistration() { - FilterRegistrationBean registration = new FilterRegistrationBean<>(); + public FilterRegistrationBean bladeFilterRegistration() { + FilterRegistrationBean registration = new FilterRegistrationBean<>(); registration.setDispatcherTypes(DispatcherType.REQUEST); - registration.setFilter(new XssFilter(xssProperties, xssUrlProperties)); + registration.setFilter(new BladeRequestFilter(requestProperties, xssProperties)); registration.addUrlPatterns("/*"); - registration.setName("xssFilter"); + registration.setName("bladeRequestFilter"); registration.setOrder(Ordered.LOWEST_PRECEDENCE); return registration; } diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/request/BladeHttpServletRequestWrapper.java b/blade-core-tool/src/main/java/org/springblade/core/tool/request/BladeHttpServletRequestWrapper.java new file mode 100644 index 0000000..c2d47e2 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/request/BladeHttpServletRequestWrapper.java @@ -0,0 +1,119 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.request; + +import org.springblade.core.tool.utils.WebUtil; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; + +/** + * 全局Request包装 + * + * @author Chill + */ +public class BladeHttpServletRequestWrapper extends HttpServletRequestWrapper { + + /** + * 没被包装过的HttpServletRequest(特殊场景,需要自己过滤) + */ + private final HttpServletRequest orgRequest; + /** + * 缓存报文,支持多次读取流 + */ + private byte[] body; + + + public BladeHttpServletRequestWrapper(HttpServletRequest request) { + super(request); + orgRequest = request; + } + + @Override + public BufferedReader getReader() throws IOException { + return new BufferedReader(new InputStreamReader(getInputStream())); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + if (super.getHeader(HttpHeaders.CONTENT_TYPE) == null) { + return super.getInputStream(); + } + + if (super.getHeader(HttpHeaders.CONTENT_TYPE).startsWith(MediaType.MULTIPART_FORM_DATA_VALUE)) { + return super.getInputStream(); + } + + if (body == null) { + body = WebUtil.getRequestBody(super.getInputStream()).getBytes(); + } + + final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body); + + return new ServletInputStream() { + + @Override + public int read() { + return byteArrayInputStream.read(); + } + + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setReadListener(ReadListener readListener) { + } + }; + } + + /** + * 获取初始request + * + * @return HttpServletRequest + */ + public HttpServletRequest getOrgRequest() { + return orgRequest; + } + + /** + * 获取初始request + * + * @param request request + * @return HttpServletRequest + */ + public static HttpServletRequest getOrgRequest(HttpServletRequest request) { + if (request instanceof BladeHttpServletRequestWrapper) { + return ((BladeHttpServletRequestWrapper) request).getOrgRequest(); + } + return request; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/support/xss/XssFilter.java b/blade-core-tool/src/main/java/org/springblade/core/tool/request/BladeRequestFilter.java similarity index 63% rename from blade-core-tool/src/main/java/org/springblade/core/tool/support/xss/XssFilter.java rename to blade-core-tool/src/main/java/org/springblade/core/tool/request/BladeRequestFilter.java index 80221de..f036d40 100644 --- a/blade-core-tool/src/main/java/org/springblade/core/tool/support/xss/XssFilter.java +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/request/BladeRequestFilter.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springblade.core.tool.support.xss; +package org.springblade.core.tool.request; import lombok.AllArgsConstructor; import org.springframework.util.AntPathMatcher; @@ -23,15 +23,15 @@ import javax.servlet.http.HttpServletRequest; import java.io.IOException; /** - * XSS过滤 + * Request全局过滤 * * @author Chill */ @AllArgsConstructor -public class XssFilter implements Filter { +public class BladeRequestFilter implements Filter { + private final RequestProperties requestProperties; private final XssProperties xssProperties; - private final XssUrlProperties xssUrlProperties; private final AntPathMatcher antPathMatcher = new AntPathMatcher(); @Override @@ -42,17 +42,28 @@ public class XssFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String path = ((HttpServletRequest) request).getServletPath(); - if (isSkip(path)) { + // 跳过 Request 包装 + if (!requestProperties.getEnabled() || isRequestSkip(path)) { chain.doFilter(request, response); - } else { + } + // 默认 Request 包装 + else if (!xssProperties.getEnabled() || isXssSkip(path)) { + BladeHttpServletRequestWrapper bladeRequest = new BladeHttpServletRequestWrapper((HttpServletRequest) request); + chain.doFilter(bladeRequest, response); + } + // Xss Request 包装 + else { XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request); chain.doFilter(xssRequest, response); } } - private boolean isSkip(String path) { - return (xssUrlProperties.getExcludePatterns().stream().anyMatch(pattern -> antPathMatcher.match(pattern, path))) - || (xssProperties.getSkipUrl().stream().anyMatch(pattern -> antPathMatcher.match(pattern, path))); + private boolean isRequestSkip(String path) { + return requestProperties.getSkipUrl().stream().anyMatch(pattern -> antPathMatcher.match(pattern, path)); + } + + private boolean isXssSkip(String path) { + return xssProperties.getSkipUrl().stream().anyMatch(pattern -> antPathMatcher.match(pattern, path)); } @Override diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/support/xss/XssUrlProperties.java b/blade-core-tool/src/main/java/org/springblade/core/tool/request/RequestProperties.java similarity index 74% rename from blade-core-tool/src/main/java/org/springblade/core/tool/support/xss/XssUrlProperties.java rename to blade-core-tool/src/main/java/org/springblade/core/tool/request/RequestProperties.java index 89f45f9..45098bb 100644 --- a/blade-core-tool/src/main/java/org/springblade/core/tool/support/xss/XssUrlProperties.java +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/request/RequestProperties.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springblade.core.tool.support.xss; +package org.springblade.core.tool.request; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -22,14 +22,22 @@ import java.util.ArrayList; import java.util.List; /** - * Xss配置类 + * Request配置类 * * @author Chill */ @Data -@ConfigurationProperties("blade.xss.url") -public class XssUrlProperties { +@ConfigurationProperties("blade.request") +public class RequestProperties { - private final List excludePatterns = new ArrayList<>(); + /** + * 开启自定义request + */ + private Boolean enabled = true; + + /** + * 放行url + */ + private List skipUrl = new ArrayList<>(); } diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/support/xss/HtmlFilter.java b/blade-core-tool/src/main/java/org/springblade/core/tool/request/XssHtmlFilter.java similarity index 98% rename from blade-core-tool/src/main/java/org/springblade/core/tool/support/xss/HtmlFilter.java rename to blade-core-tool/src/main/java/org/springblade/core/tool/request/XssHtmlFilter.java index 2393796..a8645f3 100644 --- a/blade-core-tool/src/main/java/org/springblade/core/tool/support/xss/HtmlFilter.java +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/request/XssHtmlFilter.java @@ -1,4 +1,4 @@ -package org.springblade.core.tool.support.xss; +package org.springblade.core.tool.request; import org.springblade.core.tool.utils.StringPool; @@ -41,7 +41,7 @@ import java.util.regex.Pattern; * @author Cal Hendersen * @author Michael Semb Wever */ -public final class HtmlFilter { +public final class XssHtmlFilter { /** * regex flag union representing /si modifiers in php @@ -128,7 +128,7 @@ public final class HtmlFilter { /** * Default constructor. */ - public HtmlFilter() { + public XssHtmlFilter() { vAllowed = new HashMap<>(); final ArrayList aAtts = new ArrayList(); @@ -158,7 +158,7 @@ public final class HtmlFilter { vAllowedEntities = new String[]{"amp", "gt", "lt", "quot"}; stripComment = true; encodeQuotes = true; - alwaysMakeTags = true; + alwaysMakeTags = false; } /** @@ -166,7 +166,7 @@ public final class HtmlFilter { * * @param debug turn debug on with a true argument */ - public HtmlFilter(final boolean debug) { + public XssHtmlFilter(final boolean debug) { this(); vDebug = debug; @@ -177,7 +177,7 @@ public final class HtmlFilter { * * @param conf map containing configuration. keys match field names. */ - public HtmlFilter(final Map conf) { + public XssHtmlFilter(final Map conf) { assert conf.containsKey("vAllowed") : "configuration requires vAllowed"; assert conf.containsKey("vSelfClosingTags") : "configuration requires vSelfClosingTags"; @@ -243,8 +243,8 @@ public final class HtmlFilter { s = escapeComments(s); debug(" escapeComments: " + s); - s = balanceHTML(s); - debug(" balanceHTML: " + s); + s = balanceHtml(s); + debug(" balanceHtml: " + s); s = checkTags(s); debug(" checkTags: " + s); @@ -279,7 +279,7 @@ public final class HtmlFilter { return buf.toString(); } - private String balanceHTML(String s) { + private String balanceHtml(String s) { if (alwaysMakeTags) { // // try and form html diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/support/xss/XssHttpServletRequestWrapper.java b/blade-core-tool/src/main/java/org/springblade/core/tool/request/XssHttpServletRequestWrapper.java similarity index 77% rename from blade-core-tool/src/main/java/org/springblade/core/tool/support/xss/XssHttpServletRequestWrapper.java rename to blade-core-tool/src/main/java/org/springblade/core/tool/request/XssHttpServletRequestWrapper.java index cfc8620..62255ee 100644 --- a/blade-core-tool/src/main/java/org/springblade/core/tool/support/xss/XssHttpServletRequestWrapper.java +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/request/XssHttpServletRequestWrapper.java @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springblade.core.tool.support.xss; +package org.springblade.core.tool.request; import org.springblade.core.tool.utils.StringUtil; +import org.springblade.core.tool.utils.WebUtil; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -27,12 +28,11 @@ import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; import java.util.LinkedHashMap; import java.util.Map; /** - * XSS过滤处理 + * XSS过滤 * * @author Chill */ @@ -41,12 +41,15 @@ public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper { /** * 没被包装过的HttpServletRequest(特殊场景,需要自己过滤) */ - HttpServletRequest orgRequest; - + private final HttpServletRequest orgRequest; + /** + * 缓存报文,支持多次读取流 + */ + private byte[] body; /** * html过滤 */ - private final static HtmlFilter HTML_FILTER = new HtmlFilter(); + private final static XssHtmlFilter HTML_FILTER = new XssHtmlFilter(); public XssHttpServletRequestWrapper(HttpServletRequest request) { super(request); @@ -60,7 +63,7 @@ public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper { @Override public ServletInputStream getInputStream() throws IOException { - if (null == super.getHeader(HttpHeaders.CONTENT_TYPE)) { + if (super.getHeader(HttpHeaders.CONTENT_TYPE) == null) { return super.getInputStream(); } @@ -68,7 +71,11 @@ public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper { return super.getInputStream(); } - final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(inputHandlers(super.getInputStream()).getBytes()); + if (body == null) { + body = xssEncode(WebUtil.getRequestBody(super.getInputStream())).getBytes(); + } + + final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body); return new ServletInputStream() { @@ -93,36 +100,6 @@ public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper { }; } - private String inputHandlers(ServletInputStream servletInputStream) { - StringBuilder sb = new StringBuilder(); - BufferedReader reader = null; - try { - reader = new BufferedReader(new InputStreamReader(servletInputStream, StandardCharsets.UTF_8)); - String line; - while ((line = reader.readLine()) != null) { - sb.append(line); - } - } catch (IOException e) { - e.printStackTrace(); - } finally { - if (servletInputStream != null) { - try { - servletInputStream.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - if (reader != null) { - try { - reader.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - return xssEncode(sb.toString()); - } - @Override public String getParameter(String name) { String value = super.getParameter(xssEncode(name)); @@ -138,6 +115,7 @@ public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper { if (parameters == null || parameters.length == 0) { return null; } + for (int i = 0; i < parameters.length; i++) { parameters[i] = xssEncode(parameters[i]); } @@ -172,7 +150,7 @@ public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper { } /** - * 获取最原始的request + * 获取初始request * * @return HttpServletRequest */ @@ -181,7 +159,7 @@ public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper { } /** - * 获取最原始的request + * 获取初始request * * @param request request * @return HttpServletRequest diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/support/xss/XssProperties.java b/blade-core-tool/src/main/java/org/springblade/core/tool/request/XssProperties.java similarity index 95% rename from blade-core-tool/src/main/java/org/springblade/core/tool/support/xss/XssProperties.java rename to blade-core-tool/src/main/java/org/springblade/core/tool/request/XssProperties.java index 959439e..ece890f 100644 --- a/blade-core-tool/src/main/java/org/springblade/core/tool/support/xss/XssProperties.java +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/request/XssProperties.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springblade.core.tool.support.xss; +package org.springblade.core.tool.request; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/WebUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/WebUtil.java index 8259090..27163d6 100644 --- a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/WebUtil.java +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/WebUtil.java @@ -26,11 +26,15 @@ import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.method.HandlerMethod; +import javax.servlet.ServletInputStream; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStreamReader; import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; import java.util.Enumeration; @@ -268,6 +272,77 @@ public class WebUtil extends org.springframework.web.util.WebUtils { return str.replaceAll("&", "&"); } + /** + * 获取 request 请求体 + * + * @param servletInputStream servletInputStream + * @return body + */ + public static String getRequestBody(ServletInputStream servletInputStream) { + StringBuilder sb = new StringBuilder(); + BufferedReader reader = null; + try { + reader = new BufferedReader(new InputStreamReader(servletInputStream, StandardCharsets.UTF_8)); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (servletInputStream != null) { + try { + servletInputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return sb.toString(); + } + + /** + * 获取 request 请求内容 + * + * @param request request + * @return {String} + */ + public static String getRequestContent(HttpServletRequest request) { + try { + String queryString = request.getQueryString(); + if (StringUtil.isNotBlank(queryString)) { + return new String(queryString.getBytes(Charsets.ISO_8859_1), Charsets.UTF_8).replaceAll("&", "&").replaceAll("%22", "\""); + } + String charEncoding = request.getCharacterEncoding(); + if (charEncoding == null) { + charEncoding = StringPool.UTF_8; + } + byte[] buffer = getRequestBody(request.getInputStream()).getBytes(); + String str = new String(buffer, charEncoding).trim(); + if (StringUtil.isBlank(str)) { + StringBuilder sb = new StringBuilder(); + Enumeration parameterNames = request.getParameterNames(); + while (parameterNames.hasMoreElements()) { + String key = parameterNames.nextElement(); + String value = request.getParameter(key); + StringUtil.appendBuilder(sb, key, "=", value, "&"); + } + str = StringUtil.removeSuffix(sb.toString(), "&"); + } + return str.replaceAll("&", "&"); + } catch (Exception ex) { + ex.printStackTrace(); + return StringPool.EMPTY; + } + } + }