feat(web/xss): XSS 增加过滤模式字段,提供清空、转义 2 种方式,默认使用清空模式

* update:完善XssProperties.mode属性为枚举类型
* feat(web/xss): xss增加过滤模式(mode)字段,提供清空(clean)/转义(escape)2种方式,默认使用clean模式
This commit is contained in:
whhya
2024-04-09 02:15:31 +00:00
committed by Charles7c
parent 8c91d4a26c
commit 2573fb04f0
4 changed files with 94 additions and 14 deletions

View File

@@ -65,14 +65,14 @@ public class XssFilter implements Filter {
List<String> includePatterns = xssProperties.getIncludePatterns(); List<String> includePatterns = xssProperties.getIncludePatterns();
if (CollectionUtil.isNotEmpty(includePatterns)) { if (CollectionUtil.isNotEmpty(includePatterns)) {
if (isMatchPath(request.getServletPath(), includePatterns)) { if (isMatchPath(request.getServletPath(), includePatterns)) {
filterChain.doFilter(new XssServletRequestWrapper(request), servletResponse); filterChain.doFilter(new XssServletRequestWrapper(request, xssProperties), servletResponse);
} else { } else {
filterChain.doFilter(request, servletResponse); filterChain.doFilter(request, servletResponse);
} }
return; return;
} }
// 默认:执行 XSS 过滤 // 默认:执行 XSS 过滤
filterChain.doFilter(new XssServletRequestWrapper(request), servletResponse); filterChain.doFilter(new XssServletRequestWrapper(request, xssProperties), servletResponse);
return; return;
} }
filterChain.doFilter(servletRequest, servletResponse); filterChain.doFilter(servletRequest, servletResponse);

View File

@@ -18,6 +18,7 @@ package top.charles7c.continew.starter.web.autoconfigure.xss;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import top.charles7c.continew.starter.core.constant.PropertiesConstants; import top.charles7c.continew.starter.core.constant.PropertiesConstants;
import top.charles7c.continew.starter.web.enums.XssMode;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -50,6 +51,13 @@ public class XssProperties {
*/ */
private List<String> excludePatterns = new ArrayList<>(); private List<String> excludePatterns = new ArrayList<>();
/**
* xss过滤方式
* clean : 删除xss匹配标签 (默认)
* escape 转义xss匹配标签
*/
private XssMode mode = XssMode.CLEAN;
public boolean isEnabled() { public boolean isEnabled() {
return enabled; return enabled;
} }
@@ -73,4 +81,12 @@ public class XssProperties {
public void setExcludePatterns(List<String> excludePatterns) { public void setExcludePatterns(List<String> excludePatterns) {
this.excludePatterns = excludePatterns; this.excludePatterns = excludePatterns;
} }
public XssMode getMode() {
return mode;
}
public void setMode(XssMode mode) {
this.mode = mode;
}
} }

View File

@@ -16,19 +16,24 @@
package top.charles7c.continew.starter.web.autoconfigure.xss; package top.charles7c.continew.starter.web.autoconfigure.xss;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.io.IoUtil; import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.EscapeUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HtmlUtil; import cn.hutool.http.HtmlUtil;
import jakarta.servlet.ReadListener; import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream; import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper; import jakarta.servlet.http.HttpServletRequestWrapper;
import top.charles7c.continew.starter.web.enums.XssMode;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.StringReader; import java.io.StringReader;
import java.util.List;
/** /**
* 针对 XssServletRequest 进行过滤的包装类 * 针对 XssServletRequest 进行过滤的包装类
@@ -39,17 +44,22 @@ import java.io.StringReader;
public class XssServletRequestWrapper extends HttpServletRequestWrapper { public class XssServletRequestWrapper extends HttpServletRequestWrapper {
private String body = ""; private String body = "";
private final static String ESCAPE_MODE = "ESCAPE";
String[] method = {"POST", "PATCH", "PUT"}; String[] method = {"POST", "PATCH", "PUT"};
public XssServletRequestWrapper(HttpServletRequest request) throws IOException { private final XssProperties xssProperties;
public XssServletRequestWrapper(HttpServletRequest request, XssProperties xssProperties) throws IOException {
super(request); super(request);
this.xssProperties = xssProperties;
if (StrUtil.containsAny(request.getMethod().toUpperCase(), method)) { if (StrUtil.containsAny(request.getMethod().toUpperCase(), method)) {
body = IoUtil.getReader(request.getReader()).readLine(); body = IoUtil.getReader(request.getReader()).readLine();
if (StrUtil.isNotEmpty(body)) { if (StrUtil.isEmpty(body)) {
body = cleanHtmlTag(body); return;
} }
body = handleTag(body);
} }
} }
@Override @Override
@@ -64,12 +74,12 @@ public class XssServletRequestWrapper extends HttpServletRequestWrapper {
@Override @Override
public String getQueryString() { public String getQueryString() {
return cleanHtmlTag(super.getQueryString()); return handleTag(super.getQueryString());
} }
@Override @Override
public String getParameter(String name) { public String getParameter(String name) {
return cleanHtmlTag(super.getParameter(name)); return handleTag(super.getParameter(name));
} }
@Override @Override
@@ -79,23 +89,41 @@ public class XssServletRequestWrapper extends HttpServletRequestWrapper {
return values; return values;
} }
int length = values.length; int length = values.length;
String[] escapeValues = new String[length]; String[] resultValues = new String[length];
for (int i = 0; i < length; i++) { for (int i = 0; i < length; i++) {
escapeValues[i] = cleanHtmlTag(values[i]); resultValues[i] = handleTag(values[i]);
} }
return escapeValues; return resultValues;
} }
/** /**
* 清除文本中所有HTML标签但是不删除标签内的内容 * 对文本内容使用指定过滤模式
* *
* @param content 文本内容 * @param content 文本内容
* @return 处理过后的文本 * @return 返回处理过后内容
*/ */
private String cleanHtmlTag(String content) { private String handleTag(String content) {
if (StrUtil.isEmpty(content)) { if (StrUtil.isEmpty(content)) {
return content; return content;
} }
// 获取过滤模式
XssMode mode = xssProperties.getMode();
//异常情况下mode为空则默认用清空模式
if (StrUtil.isEmpty(mode.name())) {
return HtmlUtil.cleanHtmlTag(content);
}
// 如果是escape模式则进行转义
if (mode.name().equals(ESCAPE_MODE)) {
List<String> reStr = ReUtil.findAllGroup0(HtmlUtil.RE_HTML_MARK, content);
if (CollectionUtil.isEmpty(reStr)) {
return content;
}
for (String s : reStr) {
content = content.replace(s, EscapeUtil.escapeHtml4(s).replace("\\", ""));
}
return content;
}
return HtmlUtil.cleanHtmlTag(content); return HtmlUtil.cleanHtmlTag(content);
} }
@@ -123,4 +151,5 @@ public class XssServletRequestWrapper extends HttpServletRequestWrapper {
} }
}; };
} }
} }

View File

@@ -0,0 +1,35 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <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 top.charles7c.continew.starter.web.enums;
/**
* API 类型枚举
*
* @author whhya
* @since 2.0.0
*/
public enum XssMode {
/**
* 所有 API
*/
CLEAN,
/**
* 分页
*/
ESCAPE,
}