Merge remote-tracking branch 'origin/master'

This commit is contained in:
liang.chao 2025-09-29 09:25:51 +08:00
commit 714d1a69ac
3 changed files with 189 additions and 54 deletions

View File

@ -22,13 +22,36 @@ public class SafeUtil {
* 安全脚本模式用于检测脚本注入的正则表达式 * 安全脚本模式用于检测脚本注入的正则表达式
* 由于平台中setfilter中使用多个参数时用到&符号因此未包含&符号 * 由于平台中setfilter中使用多个参数时用到&符号因此未包含&符号
*/ */
public final static String SAFE_SCRIPT_PATTERN = /*public final static String SAFE_SCRIPT_PATTERN =
"(\\||;|\\$|'|\\'|0x0d|0x0a|\\%27|\\%3B" + "(\\||;|\\$|'|\\'|0x0d|0x0a|\\%27|\\%3B" +
"|<>|\\[\\]|\\(\\)|/|\"" + "|<>|\\[\\]|\\(\\)|/|\"" +
"|script|alert|svg|confirm|prompt|onload" + "|script|alert|svg|confirm|prompt|onload" +
"|%3c|%3e|%2b|@|!|img|src" + "|%3c|%3e|%2b|@|!|img|src" +
"|%)"; "|%)";*/
// 危险字符和编码
public final static String DANGEROUS_CHARS =
"(\\||;|\\$|'|\\'|0x0d|0x0a|\\%27|\\%3B" +
"|<>|\\[\\]|\\(\\)|/|\"" +
"|%3c|%3e|%2b|@|!|%)";
// JavaScript危险函数带括号
public final static String DANGEROUS_FUNCTIONS =
"(script\\s*\\(|alert\\s*\\(|confirm\\s*\\(|prompt\\s*\\(|eval\\s*\\(" +
"|setTimeout\\s*\\(|setInterval\\s*\\()";
// 危险HTML事件
public final static String DANGEROUS_EVENTS =
"(onload\\s*=|onclick\\s*=|onerror\\s*=|onmouseover\\s*=" +
"|onfocus\\s*=|onblur\\s*=)";
// 危险标签
public final static String DANGEROUS_TAGS =
"(<script|</script>|<iframe|</iframe>|<object|</object>)";
// 完整的XSS模式
public final static String COMPLETE_XSS_PATTERN =
DANGEROUS_CHARS + "|" + DANGEROUS_FUNCTIONS + "|" +
DANGEROUS_EVENTS + "|" + DANGEROUS_TAGS;
/** /**
* 检查特殊字符的正则表达式 * 检查特殊字符的正则表达式
* 用于判断字符串是否为数字字母下划线或汉字 * 用于判断字符串是否为数字字母下划线或汉字
@ -61,6 +84,9 @@ public class SafeUtil {
return false; return false;
} }
private static final Pattern XSS_PATTERN =
Pattern.compile(SafeUtil.COMPLETE_XSS_PATTERN, Pattern.CASE_INSENSITIVE);
/** /**
* 验证字符串是否包含特殊脚本字符 * 验证字符串是否包含特殊脚本字符
* *
@ -68,10 +94,11 @@ public class SafeUtil {
* @return 如果包含特殊脚本字符返回true否则返回false * @return 如果包含特殊脚本字符返回true否则返回false
*/ */
public static boolean checkScript(String mark) { public static boolean checkScript(String mark) {
if (mark != null && !"".equals(mark)) { /*if (mark != null && !"".equals(mark)) {
return match(SAFE_SCRIPT_PATTERN, mark.toLowerCase().trim()); return match(SAFE_SCRIPT_PATTERN, mark.toLowerCase().trim());
} }*/
return false; if (mark == null) return false;
return XSS_PATTERN.matcher(mark).find();
} }
/** /**

View File

@ -4,6 +4,7 @@ import com.alibaba.fastjson2.JSON;
import com.bonus.common.core.domain.AjaxResult; import com.bonus.common.core.domain.AjaxResult;
import com.bonus.common.utils.SafeUtil; import com.bonus.common.utils.SafeUtil;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.servlet.AsyncHandlerInterceptor; import org.springframework.web.servlet.AsyncHandlerInterceptor;
@ -69,7 +70,7 @@ public class ParamSecureHandler implements AsyncHandlerInterceptor {
String params = request.getParameter("params"); String params = request.getParameter("params");
boolean flag = XssCheck.xssCleanNew(params); boolean flag = XssCheck.xssCleanNew(params);
if(!flag){ if(!flag){
returnJson(response, "输入值非法", 500); returnJson(response, "输入值非法", 500,null);
return false; return false;
} }
return true; return true;
@ -81,10 +82,20 @@ public class ParamSecureHandler implements AsyncHandlerInterceptor {
* 校验参数是否合法 * 校验参数是否合法
*/ */
if (!requestWrapper.isChecked()) { if (!requestWrapper.isChecked()) {
log.error("输入值非法: queryString={}, body={}", /*log.error("输入值非法: queryString={}, body={}",
StringUtils.defaultString(requestWrapper.getQueryString(), "null"), StringUtils.defaultString(requestWrapper.getQueryString(), "null"),
StringUtils.defaultString(requestWrapper.getReaderParam(), "null")); StringUtils.defaultString(requestWrapper.getReaderParam(), "null"));*/
returnJson(response, "输入值非法", 500);
Map<String, Object> map = new HashMap<>(16);
List<XssRequestWrapper.IllegalParameter> illegalParameters = requestWrapper.getIllegalParameters();
if(CollectionUtils.isNotEmpty(illegalParameters)){
log.error("非法参数{}",illegalParameters);
XssRequestWrapper.IllegalParameter illegalParameter = illegalParameters.get(0);
map.put("paramName",illegalParameter.getParamName().replace("REQUEST_BODY.",""));
map.put("originalValue",illegalParameter.getOriginalValue());
}
returnJson(response, "输入值非法", 500,map);
return false; return false;
} }
// System.err.println(JSON.toJSONString(request.getParameterMap())); // System.err.println(JSON.toJSONString(request.getParameterMap()));
@ -95,7 +106,7 @@ public class ParamSecureHandler implements AsyncHandlerInterceptor {
Map<String, String[]> map = requestWrapper.getParameterMap(); Map<String, String[]> map = requestWrapper.getParameterMap();
boolean checkParameterMap = checkParameterMap(map, requestUrl); boolean checkParameterMap = checkParameterMap(map, requestUrl);
if (!checkParameterMap) { if (!checkParameterMap) {
returnJson(response, "输入值非法", 500); returnJson(response, "输入值非法", 500,null);
return false; return false;
} }
@ -108,7 +119,7 @@ public class ParamSecureHandler implements AsyncHandlerInterceptor {
if (!requestUrl.contains(uplFile) && !requestUrl.contains(upImage) && !requestUrl.contains(path)) { if (!requestUrl.contains(uplFile) && !requestUrl.contains(upImage) && !requestUrl.contains(path)) {
boolean checkReader = checkReader(readerParam, requestUrl); boolean checkReader = checkReader(readerParam, requestUrl);
if (!checkReader) { if (!checkReader) {
returnJson(response, "不安全参数", 500); returnJson(response, "不安全参数", 500,null);
return false; return false;
} }
} }
@ -116,12 +127,12 @@ public class ParamSecureHandler implements AsyncHandlerInterceptor {
} }
private void returnJson(HttpServletResponse response, String msg, int code) { private void returnJson(HttpServletResponse response, String msg, int code,Object data) {
response.reset(); response.reset();
PrintWriter writer = null; PrintWriter writer = null;
response.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8");
response.setContentType("applicatiopn/json;charset=utf-8"); response.setContentType("applicatiopn/json;charset=utf-8");
AjaxResult a = AjaxResult.error(code, msg); AjaxResult a = AjaxResult.error(msg,data);
String res = JSON.toJSONString(a); String res = JSON.toJSONString(a);
try { try {
writer = response.getWriter(); writer = response.getWriter();

View File

@ -1,5 +1,6 @@
package com.bonus.framework.interceptor; package com.bonus.framework.interceptor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
@ -13,10 +14,7 @@ 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.util.ArrayList; import java.util.*;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/** /**
@ -31,6 +29,9 @@ public class XssRequestWrapper extends HttpServletRequestWrapper {
private String streamParam; private String streamParam;
private boolean checked; private boolean checked;
// 新增记录非法参数和攻击内容
private List<IllegalParameter> illegalParameters = new ArrayList<>();
// 预编译所有正则表达式模式以提高性能 // 预编译所有正则表达式模式以提高性能
private static final List<Pattern> XSS_PATTERNS = new ArrayList<>(); private static final List<Pattern> XSS_PATTERNS = new ArrayList<>();
@ -114,7 +115,9 @@ public class XssRequestWrapper extends HttpServletRequestWrapper {
// 检查安全性仅校验不篡改 // 检查安全性仅校验不篡改
String queryStr = request.getQueryString(); String queryStr = request.getQueryString();
setChecked(xssCleanNew(requestBody) && (queryStr == null || xssCleanNew(queryStr))); boolean isBodySafe = xssCleanNew("REQUEST_BODY", requestBody);
boolean isQuerySafe = queryStr == null || xssCleanNew("QUERY_STRING", queryStr);
setChecked(isBodySafe && isQuerySafe);
} catch (IOException e) { } catch (IOException e) {
log.error("读取请求体数据失败: {}", e.getLocalizedMessage(), e); log.error("读取请求体数据失败: {}", e.getLocalizedMessage(), e);
@ -124,7 +127,7 @@ public class XssRequestWrapper extends HttpServletRequestWrapper {
} }
// 不对查询串做篡改保持原样防止影响参数绑定 // 不对查询串做篡改保持原样防止影响参数绑定
queryString = request.getQueryString(); queryString = super.getQueryString();
} }
@Override @Override
@ -173,7 +176,7 @@ public class XssRequestWrapper extends HttpServletRequestWrapper {
String[] values = entry.getValue(); String[] values = entry.getValue();
for (int i = 0; i < values.length; i++) { for (int i = 0; i < values.length; i++) {
if (values[i] != null) { if (values[i] != null) {
values[i] = xssClean(values[i]); values[i] = xssClean(entry.getKey(), values[i]);
} }
} }
} }
@ -191,7 +194,7 @@ public class XssRequestWrapper extends HttpServletRequestWrapper {
String[] cleanedValues = new String[values.length]; String[] cleanedValues = new String[values.length];
for (int i = 0; i < values.length; i++) { for (int i = 0; i < values.length; i++) {
if (values[i] != null) { if (values[i] != null) {
cleanedValues[i] = xssClean(values[i]); cleanedValues[i] = xssClean(paramString, values[i]);
} }
} }
return cleanedValues; return cleanedValues;
@ -203,7 +206,7 @@ public class XssRequestWrapper extends HttpServletRequestWrapper {
if (value == null) { if (value == null) {
return null; return null;
} }
return xssClean(value); return xssClean(paramString, value);
} }
@Override @Override
@ -212,22 +215,29 @@ public class XssRequestWrapper extends HttpServletRequestWrapper {
if (value == null) { if (value == null) {
return null; return null;
} }
return xssClean(value); return xssClean(paramString, value);
} }
private String xssClean(String value) { private String xssClean(String paramName, String value) {
if (value == null) { if (value == null) {
return null; return null;
} }
String cleanedValue = value;
// 使用预编译的模式 // 使用预编译的模式
for (Pattern pattern : XSS_PATTERNS) { for (Pattern pattern : XSS_PATTERNS) {
value = pattern.matcher(value).replaceAll(""); cleanedValue = pattern.matcher(cleanedValue).replaceAll("");
} }
return value;
// 如果值被修改记录非法参数
if (!cleanedValue.equals(value)) {
recordIllegalParameter(paramName, value, cleanedValue, "XSS_ATTACK");
}
return cleanedValue;
} }
private boolean xssCleanNew(String value) { private boolean xssCleanNew(String paramName, String value) {
if (value == null) { if (value == null) {
return true; return true;
} }
@ -235,13 +245,15 @@ public class XssRequestWrapper extends HttpServletRequestWrapper {
// 首先检查是否为JSON格式 // 首先检查是否为JSON格式
if (isJsonString(value)) { if (isJsonString(value)) {
// 对JSON字符串进行特殊处理检查值部分是否包含XSS // 对JSON字符串进行特殊处理检查值部分是否包含XSS
return isJsonSafe(value); return isJsonSafe(paramName, value);
} }
// 使用预编译的模式进行检查 // 使用预编译的模式进行检查
for (Pattern pattern : XSS_PATTERNS) { for (Pattern pattern : XSS_PATTERNS) {
if (match(pattern, value)) { if (match(pattern, value)) {
return false; // 发现XSS攻击 // 发现XSS攻击记录非法参数
recordIllegalParameter(paramName, value, null, "XSS_PATTERN_" + pattern.pattern());
return false;
} }
} }
return true; // 安全 return true; // 安全
@ -274,91 +286,101 @@ public class XssRequestWrapper extends HttpServletRequestWrapper {
} }
// 检查JSON字符串中的值是否安全 // 检查JSON字符串中的值是否安全
private boolean isJsonSafe(String jsonStr) { private boolean isJsonSafe(String paramName, String jsonStr) {
try { try {
// 处理JSON对象 // 处理JSON对象
if (jsonStr.trim().startsWith("{")) { if (jsonStr.trim().startsWith("{")) {
JSONObject jsonObject = new JSONObject(jsonStr); JSONObject jsonObject = new JSONObject(jsonStr);
return isJsonObjectSafe(jsonObject); return isJsonObjectSafe(paramName, jsonObject);
} }
// 处理JSON数组 // 处理JSON数组
else if (jsonStr.trim().startsWith("[")) { else if (jsonStr.trim().startsWith("[")) {
JSONArray jsonArray = new JSONArray(jsonStr); JSONArray jsonArray = new JSONArray(jsonStr);
return isJsonArraySafe(jsonArray); return isJsonArraySafe(paramName, jsonArray);
} }
return true; return true;
} catch (JSONException e) { } catch (JSONException e) {
// 解析失败回退到普通字符串检查 // 解析失败回退到普通字符串检查
return xssCleanNewFallback(jsonStr); return xssCleanNewFallback(paramName, jsonStr);
} }
} }
// 递归检查JSON对象的安全性 // 递归检查JSON对象的安全性
private boolean isJsonObjectSafe(JSONObject jsonObject) throws JSONException { private boolean isJsonObjectSafe(String parentKey, JSONObject jsonObject) throws JSONException {
Iterator<String> keys = jsonObject.keys(); Iterator<String> keys = jsonObject.keys();
boolean isSafe = true;
while (keys.hasNext()) { while (keys.hasNext()) {
String key = keys.next(); String key = keys.next();
Object value = jsonObject.get(key); Object value = jsonObject.get(key);
String fullKey = parentKey + "." + key;
// 检查key的安全性 // 检查key的安全性
if (!xssCleanNewFallback(key)) { if (!xssCleanNewFallback(fullKey + "[KEY]", key)) {
return false; isSafe = false;
} }
// 检查value的安全性 // 检查value的安全性
if (value instanceof String) { if (value instanceof String) {
if (!xssCleanNewFallback((String) value)) { if (!xssCleanNewFallback(fullKey, (String) value)) {
return false; isSafe = false;
} }
} else if (value instanceof JSONObject) { } else if (value instanceof JSONObject) {
if (!isJsonObjectSafe((JSONObject) value)) { if (!isJsonObjectSafe(fullKey, (JSONObject) value)) {
return false; isSafe = false;
} }
} else if (value instanceof JSONArray) { } else if (value instanceof JSONArray) {
if (!isJsonArraySafe((JSONArray) value)) { if (!isJsonArraySafe(fullKey, (JSONArray) value)) {
return false; isSafe = false;
} }
} }
// 其他类型数字布尔值等视为安全 // 其他类型数字布尔值等视为安全
} }
return true; return isSafe;
} }
// 递归检查JSON数组的安全性 // 递归检查JSON数组的安全性
private boolean isJsonArraySafe(JSONArray jsonArray) throws JSONException { private boolean isJsonArraySafe(String parentKey, JSONArray jsonArray) throws JSONException {
boolean isSafe = true;
for (int i = 0; i < jsonArray.length(); i++) { for (int i = 0; i < jsonArray.length(); i++) {
Object value = jsonArray.get(i); Object value = jsonArray.get(i);
String fullKey = parentKey + "[" + i + "]";
if (value instanceof String) { if (value instanceof String) {
if (!xssCleanNewFallback((String) value)) { if (!xssCleanNewFallback(fullKey, (String) value)) {
return false; isSafe = false;
} }
} else if (value instanceof JSONObject) { } else if (value instanceof JSONObject) {
if (!isJsonObjectSafe((JSONObject) value)) { if (!isJsonObjectSafe(fullKey, (JSONObject) value)) {
return false; isSafe = false;
} }
} else if (value instanceof JSONArray) { } else if (value instanceof JSONArray) {
if (!isJsonArraySafe((JSONArray) value)) { if (!isJsonArraySafe(fullKey, (JSONArray) value)) {
return false; isSafe = false;
} }
} }
// 其他类型数字布尔值等视为安全 // 其他类型数字布尔值等视为安全
} }
return true; return isSafe;
} }
// 回退到原始的模式匹配避免递归调用 // 回退到原始的模式匹配避免递归调用
private boolean xssCleanNewFallback(String value) { private boolean xssCleanNewFallback(String paramName, String value) {
if (value == null) { if (value == null) {
return true; return true;
} }
boolean isSafe = true;
for (Pattern pattern : XSS_PATTERNS) { for (Pattern pattern : XSS_PATTERNS) {
if (match(pattern, value)) { if (match(pattern, value)) {
return false; recordIllegalParameter(paramName, value, null, "XSS_PATTERN_" + pattern.pattern());
isSafe = false;
// 继续检查其他模式记录所有匹配的攻击模式
} }
} }
return true; return isSafe;
} }
/** /**
@ -368,6 +390,46 @@ public class XssRequestWrapper extends HttpServletRequestWrapper {
return pattern.matcher(str).find(); return pattern.matcher(str).find();
} }
/**
* 记录非法参数信息
*/
private void recordIllegalParameter(String paramName, String originalValue, String cleanedValue, String attackType) {
IllegalParameter illegalParam = new IllegalParameter();
illegalParam.setParamName(paramName);
illegalParam.setOriginalValue(originalValue);
illegalParam.setCleanedValue(cleanedValue);
illegalParam.setAttackType(attackType);
illegalParam.setDetectedTime(new Date());
illegalParam.setRequestUrl(super.getRequestURL().toString());
illegalParam.setClientIp(getClientIp());
illegalParameters.add(illegalParam);
// 记录警告日志
/*log.warn("检测到XSS攻击尝试 - 参数: {}, 攻击类型: {}, 原始值: {}, 客户端IP: {}",
paramName, attackType,
originalValue.length() > 100 ? originalValue.substring(0, 100) + "..." : originalValue,
illegalParam.getClientIp());*/
}
/**
* 获取客户端IP
*/
private String getClientIp() {
HttpServletRequest request = (HttpServletRequest) super.getRequest();
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
public String getReaderParam() { public String getReaderParam() {
return streamParam; return streamParam;
} }
@ -380,9 +442,44 @@ public class XssRequestWrapper extends HttpServletRequestWrapper {
this.checked = checked; this.checked = checked;
} }
// 新增获取非法参数列表
public List<IllegalParameter> getIllegalParameters() {
return Collections.unmodifiableList(illegalParameters);
}
// 新增检查是否有非法参数
public boolean hasIllegalParameters() {
return !illegalParameters.isEmpty();
}
// 新增获取非法参数数量
public int getIllegalParameterCount() {
return illegalParameters.size();
}
// 新增清空非法参数记录用于重置
public void clearIllegalParameters() {
illegalParameters.clear();
}
// 添加字符编码支持 // 添加字符编码支持
public String getCharacterEncoding() { public String getCharacterEncoding() {
String encoding = super.getCharacterEncoding(); String encoding = super.getCharacterEncoding();
return encoding != null ? encoding : "UTF-8"; return encoding != null ? encoding : "UTF-8";
} }
/**
* 非法参数信息类
*/
@Data
public static class IllegalParameter {
private String paramName; // 参数名
private String originalValue; // 原始值
private String cleanedValue; // 清理后的值可能为null
private String attackType; // 攻击类型
private Date detectedTime; // 检测时间
private String requestUrl; // 请求URL
private String clientIp; // 客户端IP
}
} }