优化Xss运行逻辑

This commit is contained in:
smallchill 2022-03-01 23:49:39 +08:00
parent edc1f5d138
commit c42b165f27
8 changed files with 267 additions and 80 deletions

View File

@ -16,10 +16,9 @@
package org.springblade.core.tool.config; package org.springblade.core.tool.config;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import org.springblade.core.tool.support.xss.XssFilter; import org.springblade.core.tool.request.BladeRequestFilter;
import org.springblade.core.tool.support.xss.XssProperties; import org.springblade.core.tool.request.RequestProperties;
import org.springblade.core.tool.support.xss.XssUrlProperties; import org.springblade.core.tool.request.XssProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -29,31 +28,28 @@ import org.springframework.core.Ordered;
import javax.servlet.DispatcherType; import javax.servlet.DispatcherType;
/** /**
* Xss配置类 * 过滤器配置类
* *
* @author Chill * @author Chill
*/ */
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@AllArgsConstructor @AllArgsConstructor
@ConditionalOnProperty(value = "blade.xss.enabled", havingValue = "true") @EnableConfigurationProperties({RequestProperties.class, XssProperties.class})
@EnableConfigurationProperties({XssProperties.class, XssUrlProperties.class}) public class RequestConfiguration {
public class XssConfiguration {
private final RequestProperties requestProperties;
private final XssProperties xssProperties; private final XssProperties xssProperties;
private final XssUrlProperties xssUrlProperties;
/** /**
* 防XSS注入 * 全局过滤器
*
* @return FilterRegistrationBean
*/ */
@Bean @Bean
public FilterRegistrationBean<XssFilter> xssFilterRegistration() { public FilterRegistrationBean<BladeRequestFilter> bladeFilterRegistration() {
FilterRegistrationBean<XssFilter> registration = new FilterRegistrationBean<>(); FilterRegistrationBean<BladeRequestFilter> registration = new FilterRegistrationBean<>();
registration.setDispatcherTypes(DispatcherType.REQUEST); registration.setDispatcherTypes(DispatcherType.REQUEST);
registration.setFilter(new XssFilter(xssProperties, xssUrlProperties)); registration.setFilter(new BladeRequestFilter(requestProperties, xssProperties));
registration.addUrlPatterns("/*"); registration.addUrlPatterns("/*");
registration.setName("xssFilter"); registration.setName("bladeRequestFilter");
registration.setOrder(Ordered.LOWEST_PRECEDENCE); registration.setOrder(Ordered.LOWEST_PRECEDENCE);
return registration; return registration;
} }

View File

@ -0,0 +1,119 @@
/**
* Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com).
* <p>
* 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
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* 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;
}
}

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springblade.core.tool.support.xss; package org.springblade.core.tool.request;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import org.springframework.util.AntPathMatcher; import org.springframework.util.AntPathMatcher;
@ -23,15 +23,15 @@ import javax.servlet.http.HttpServletRequest;
import java.io.IOException; import java.io.IOException;
/** /**
* XSS过滤 * Request全局过滤
* *
* @author Chill * @author Chill
*/ */
@AllArgsConstructor @AllArgsConstructor
public class XssFilter implements Filter { public class BladeRequestFilter implements Filter {
private final RequestProperties requestProperties;
private final XssProperties xssProperties; private final XssProperties xssProperties;
private final XssUrlProperties xssUrlProperties;
private final AntPathMatcher antPathMatcher = new AntPathMatcher(); private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override @Override
@ -42,17 +42,28 @@ public class XssFilter implements Filter {
@Override @Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String path = ((HttpServletRequest) request).getServletPath(); String path = ((HttpServletRequest) request).getServletPath();
if (isSkip(path)) { // 跳过 Request 包装
if (!requestProperties.getEnabled() || isRequestSkip(path)) {
chain.doFilter(request, response); 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); XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request);
chain.doFilter(xssRequest, response); chain.doFilter(xssRequest, response);
} }
} }
private boolean isSkip(String path) { private boolean isRequestSkip(String path) {
return (xssUrlProperties.getExcludePatterns().stream().anyMatch(pattern -> antPathMatcher.match(pattern, path))) return requestProperties.getSkipUrl().stream().anyMatch(pattern -> antPathMatcher.match(pattern, path));
|| (xssProperties.getSkipUrl().stream().anyMatch(pattern -> antPathMatcher.match(pattern, path))); }
private boolean isXssSkip(String path) {
return xssProperties.getSkipUrl().stream().anyMatch(pattern -> antPathMatcher.match(pattern, path));
} }
@Override @Override

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springblade.core.tool.support.xss; package org.springblade.core.tool.request;
import lombok.Data; import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
@ -22,14 +22,22 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
* Xss配置类 * Request配置类
* *
* @author Chill * @author Chill
*/ */
@Data @Data
@ConfigurationProperties("blade.xss.url") @ConfigurationProperties("blade.request")
public class XssUrlProperties { public class RequestProperties {
private final List<String> excludePatterns = new ArrayList<>(); /**
* 开启自定义request
*/
private Boolean enabled = true;
/**
* 放行url
*/
private List<String> skipUrl = new ArrayList<>();
} }

View File

@ -1,4 +1,4 @@
package org.springblade.core.tool.support.xss; package org.springblade.core.tool.request;
import org.springblade.core.tool.utils.StringPool; import org.springblade.core.tool.utils.StringPool;
@ -41,7 +41,7 @@ import java.util.regex.Pattern;
* @author Cal Hendersen * @author Cal Hendersen
* @author Michael Semb Wever * @author Michael Semb Wever
*/ */
public final class HtmlFilter { public final class XssHtmlFilter {
/** /**
* regex flag union representing /si modifiers in php * regex flag union representing /si modifiers in php
@ -128,7 +128,7 @@ public final class HtmlFilter {
/** /**
* Default constructor. * Default constructor.
*/ */
public HtmlFilter() { public XssHtmlFilter() {
vAllowed = new HashMap<>(); vAllowed = new HashMap<>();
final ArrayList<String> aAtts = new ArrayList<String>(); final ArrayList<String> aAtts = new ArrayList<String>();
@ -158,7 +158,7 @@ public final class HtmlFilter {
vAllowedEntities = new String[]{"amp", "gt", "lt", "quot"}; vAllowedEntities = new String[]{"amp", "gt", "lt", "quot"};
stripComment = true; stripComment = true;
encodeQuotes = true; encodeQuotes = true;
alwaysMakeTags = true; alwaysMakeTags = false;
} }
/** /**
@ -166,7 +166,7 @@ public final class HtmlFilter {
* *
* @param debug turn debug on with a true argument * @param debug turn debug on with a true argument
*/ */
public HtmlFilter(final boolean debug) { public XssHtmlFilter(final boolean debug) {
this(); this();
vDebug = debug; vDebug = debug;
@ -177,7 +177,7 @@ public final class HtmlFilter {
* *
* @param conf map containing configuration. keys match field names. * @param conf map containing configuration. keys match field names.
*/ */
public HtmlFilter(final Map<String, Object> conf) { public XssHtmlFilter(final Map<String, Object> conf) {
assert conf.containsKey("vAllowed") : "configuration requires vAllowed"; assert conf.containsKey("vAllowed") : "configuration requires vAllowed";
assert conf.containsKey("vSelfClosingTags") : "configuration requires vSelfClosingTags"; assert conf.containsKey("vSelfClosingTags") : "configuration requires vSelfClosingTags";
@ -243,8 +243,8 @@ public final class HtmlFilter {
s = escapeComments(s); s = escapeComments(s);
debug(" escapeComments: " + s); debug(" escapeComments: " + s);
s = balanceHTML(s); s = balanceHtml(s);
debug(" balanceHTML: " + s); debug(" balanceHtml: " + s);
s = checkTags(s); s = checkTags(s);
debug(" checkTags: " + s); debug(" checkTags: " + s);
@ -279,7 +279,7 @@ public final class HtmlFilter {
return buf.toString(); return buf.toString();
} }
private String balanceHTML(String s) { private String balanceHtml(String s) {
if (alwaysMakeTags) { if (alwaysMakeTags) {
// //
// try and form html // try and form html

View File

@ -13,9 +13,10 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * 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.StringUtil;
import org.springblade.core.tool.utils.WebUtil;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@ -27,12 +28,11 @@ import java.io.BufferedReader;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
/** /**
* XSS过滤处理 * XSS过滤
* *
* @author Chill * @author Chill
*/ */
@ -41,12 +41,15 @@ public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
/** /**
* 没被包装过的HttpServletRequest特殊场景,需要自己过滤 * 没被包装过的HttpServletRequest特殊场景,需要自己过滤
*/ */
HttpServletRequest orgRequest; private final HttpServletRequest orgRequest;
/**
* 缓存报文,支持多次读取流
*/
private byte[] body;
/** /**
* html过滤 * html过滤
*/ */
private final static HtmlFilter HTML_FILTER = new HtmlFilter(); private final static XssHtmlFilter HTML_FILTER = new XssHtmlFilter();
public XssHttpServletRequestWrapper(HttpServletRequest request) { public XssHttpServletRequestWrapper(HttpServletRequest request) {
super(request); super(request);
@ -60,7 +63,7 @@ public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
@Override @Override
public ServletInputStream getInputStream() throws IOException { public ServletInputStream getInputStream() throws IOException {
if (null == super.getHeader(HttpHeaders.CONTENT_TYPE)) { if (super.getHeader(HttpHeaders.CONTENT_TYPE) == null) {
return super.getInputStream(); return super.getInputStream();
} }
@ -68,7 +71,11 @@ public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
return super.getInputStream(); 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() { 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 @Override
public String getParameter(String name) { public String getParameter(String name) {
String value = super.getParameter(xssEncode(name)); String value = super.getParameter(xssEncode(name));
@ -138,6 +115,7 @@ public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
if (parameters == null || parameters.length == 0) { if (parameters == null || parameters.length == 0) {
return null; return null;
} }
for (int i = 0; i < parameters.length; i++) { for (int i = 0; i < parameters.length; i++) {
parameters[i] = xssEncode(parameters[i]); parameters[i] = xssEncode(parameters[i]);
} }
@ -172,7 +150,7 @@ public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
} }
/** /**
* 获取最原始的request * 获取初始request
* *
* @return HttpServletRequest * @return HttpServletRequest
*/ */
@ -181,7 +159,7 @@ public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
} }
/** /**
* 获取最原始的request * 获取初始request
* *
* @param request request * @param request request
* @return HttpServletRequest * @return HttpServletRequest

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springblade.core.tool.support.xss; package org.springblade.core.tool.request;
import lombok.Data; import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;

View File

@ -26,11 +26,15 @@ import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.HandlerMethod;
import javax.servlet.ServletInputStream;
import javax.servlet.http.Cookie; import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration; import java.util.Enumeration;
@ -268,6 +272,77 @@ public class WebUtil extends org.springframework.web.util.WebUtils {
return str.replaceAll("&amp;", "&"); return str.replaceAll("&amp;", "&");
} }
/**
* 获取 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("&amp;", "&").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<String> 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("&amp;", "&");
} catch (Exception ex) {
ex.printStackTrace();
return StringPool.EMPTY;
}
}
} }