优化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;
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<XssFilter> xssFilterRegistration() {
FilterRegistrationBean<XssFilter> registration = new FilterRegistrationBean<>();
public FilterRegistrationBean<BladeRequestFilter> bladeFilterRegistration() {
FilterRegistrationBean<BladeRequestFilter> 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;
}

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
* 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

View File

@ -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<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;
@ -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<String> aAtts = new ArrayList<String>();
@ -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<String, Object> conf) {
public XssHtmlFilter(final Map<String, Object> 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

View File

@ -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

View File

@ -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;

View File

@ -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("&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;
}
}
}