feat(security/xss): 新增 XSS 过滤模块(原 web 模块内组件)

This commit is contained in:
2025-03-26 20:41:20 +08:00
parent 3fc9d1fbaa
commit b5bfe5c681
8 changed files with 46 additions and 9 deletions

View File

@@ -1,48 +0,0 @@
/*
* 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.continew.starter.web.autoconfigure.xss;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import top.continew.starter.core.constant.PropertiesConstants;
/**
* XSS 过滤自动配置
*
* @author whhya
* @since 2.0.0
*/
@AutoConfiguration
@ConditionalOnWebApplication
@EnableConfigurationProperties(XssProperties.class)
@ConditionalOnProperty(prefix = PropertiesConstants.WEB_XSS, name = PropertiesConstants.ENABLED, havingValue = "true")
public class XssAutoConfiguration {
/**
* XSS 过滤器配置
*/
@Bean
public FilterRegistrationBean<XssFilter> xssFilter(XssProperties xssProperties) {
FilterRegistrationBean<XssFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new XssFilter(xssProperties));
return registrationBean;
}
}

View File

@@ -1,79 +0,0 @@
/*
* 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.continew.starter.web.autoconfigure.xss;
import cn.hutool.core.collection.CollUtil;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import top.continew.starter.web.util.SpringWebUtils;
import java.io.IOException;
import java.util.List;
/**
* XSS 过滤器
*
* @author whhya
* @since 2.0.0
*/
public class XssFilter implements Filter {
private static final Logger log = LoggerFactory.getLogger(XssFilter.class);
private final XssProperties xssProperties;
public XssFilter(XssProperties xssProperties) {
this.xssProperties = xssProperties;
}
@Override
public void init(FilterConfig filterConfig) {
log.debug("[ContiNew Starter] - Auto Configuration 'Web-XssFilter' completed initialization.");
}
@Override
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
// 未开启 XSS 过滤,则直接跳过
if (servletRequest instanceof HttpServletRequest request && xssProperties.isEnabled()) {
// 放行路由:忽略 XSS 过滤
List<String> excludePatterns = xssProperties.getExcludePatterns();
if (CollUtil.isNotEmpty(excludePatterns) && SpringWebUtils.isMatch(request
.getServletPath(), excludePatterns)) {
filterChain.doFilter(request, servletResponse);
return;
}
// 拦截路由:执行 XSS 过滤
List<String> includePatterns = xssProperties.getIncludePatterns();
if (CollUtil.isNotEmpty(includePatterns)) {
if (SpringWebUtils.isMatch(request.getServletPath(), includePatterns)) {
filterChain.doFilter(new XssServletRequestWrapper(request, xssProperties), servletResponse);
} else {
filterChain.doFilter(request, servletResponse);
}
return;
}
// 默认:执行 XSS 过滤
filterChain.doFilter(new XssServletRequestWrapper(request, xssProperties), servletResponse);
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
}

View File

@@ -1,90 +0,0 @@
/*
* 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.continew.starter.web.autoconfigure.xss;
import org.springframework.boot.context.properties.ConfigurationProperties;
import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.web.enums.XssMode;
import java.util.ArrayList;
import java.util.List;
/**
* XSS 过滤配置属性
*
* @author whhya
* @since 2.0.0
*/
@ConfigurationProperties(PropertiesConstants.WEB_XSS)
public class XssProperties {
/**
* 是否启用 XSS 过滤
*/
private boolean enabled = true;
/**
* 拦截路由(默认为空)
*
* <p>
* 当拦截的路由配置不为空,则根据该配置执行过滤
* </p>
*/
private List<String> includePatterns = new ArrayList<>();
/**
* 放行路由(默认为空)
*/
private List<String> excludePatterns = new ArrayList<>();
/**
* XSS 模式
*/
private XssMode mode = XssMode.CLEAN;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public List<String> getIncludePatterns() {
return includePatterns;
}
public void setIncludePatterns(List<String> includePatterns) {
this.includePatterns = includePatterns;
}
public List<String> getExcludePatterns() {
return excludePatterns;
}
public void setExcludePatterns(List<String> excludePatterns) {
this.excludePatterns = excludePatterns;
}
public XssMode getMode() {
return mode;
}
public void setMode(XssMode mode) {
this.mode = mode;
}
}

View File

@@ -1,151 +0,0 @@
/*
* 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.continew.starter.web.autoconfigure.xss;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.EscapeUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.http.HtmlUtil;
import cn.hutool.http.Method;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.web.enums.XssMode;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.util.List;
/**
* 针对 XssServletRequest 进行过滤的包装类
*
* @author whhya
* @since 2.0.0
*/
public class XssServletRequestWrapper extends HttpServletRequestWrapper {
private final XssProperties xssProperties;
private String body = "";
public XssServletRequestWrapper(HttpServletRequest request, XssProperties xssProperties) throws IOException {
super(request);
this.xssProperties = xssProperties;
if (CharSequenceUtil.equalsAnyIgnoreCase(request.getMethod().toUpperCase(), Method.POST.name(), Method.PATCH
.name(), Method.PUT.name())) {
body = IoUtil.getReader(request.getReader()).readLine();
if (CharSequenceUtil.isBlank(body)) {
return;
}
body = this.handleTag(body);
}
}
@Override
public BufferedReader getReader() {
return IoUtil.toBuffered(new StringReader(body));
}
@Override
public ServletInputStream getInputStream() {
return getServletInputStream(body);
}
@Override
public String getQueryString() {
return this.handleTag(super.getQueryString());
}
@Override
public String getParameter(String name) {
return this.handleTag(super.getParameter(name));
}
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name);
if (ArrayUtil.isEmpty(values)) {
return values;
}
int length = values.length;
String[] resultValues = new String[length];
for (int i = 0; i < length; i++) {
resultValues[i] = this.handleTag(values[i]);
}
return resultValues;
}
/**
* 对文本内容进行 XSS 处理
*
* @param content 文本内容
* @return 返回处理过后内容
*/
private String handleTag(String content) {
if (CharSequenceUtil.isBlank(content)) {
return content;
}
XssMode mode = xssProperties.getMode();
// 转义
if (XssMode.ESCAPE.equals(mode)) {
List<String> reStr = ReUtil.findAllGroup0(HtmlUtil.RE_HTML_MARK, content);
if (CollUtil.isEmpty(reStr)) {
return content;
}
for (String s : reStr) {
content = content.replace(s, EscapeUtil.escapeHtml4(s)
.replace(StringConstants.BACKSLASH, StringConstants.EMPTY));
}
return content;
}
// 清理
return HtmlUtil.cleanHtmlTag(content);
}
static ServletInputStream getServletInputStream(String body) {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
return new ServletInputStream() {
@Override
public int read() {
return byteArrayInputStream.read();
}
@Override
public boolean isFinished() {
return byteArrayInputStream.available() == 0;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
// 设置监听器
}
};
}
}

View File

@@ -1,36 +0,0 @@
/*
* 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.continew.starter.web.enums;
/**
* XSS 模式枚举
*
* @author whhya
* @since 2.0.0
*/
public enum XssMode {
/**
* 清理
*/
CLEAN,
/**
* 转义
*/
ESCAPE,
}