mirror of
https://github.com/chillzhuang/blade-tool
synced 2024-12-12 12:19:27 +08:00
⚡ 优化Xss运行逻辑
This commit is contained in:
parent
edc1f5d138
commit
c42b165f27
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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
|
@ -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<>();
|
||||
|
||||
}
|
@ -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
|
@ -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
|
@ -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;
|
@ -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<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("&", "&");
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
return StringPool.EMPTY;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user