diff --git a/bonus-common/src/main/java/com/bonus/common/utils/SafeUtil.java b/bonus-common/src/main/java/com/bonus/common/utils/SafeUtil.java index 4e69712..8cb21ef 100644 --- a/bonus-common/src/main/java/com/bonus/common/utils/SafeUtil.java +++ b/bonus-common/src/main/java/com/bonus/common/utils/SafeUtil.java @@ -22,13 +22,36 @@ public class SafeUtil { * 安全脚本模式,用于检测脚本注入的正则表达式 * 由于平台中setfilter中使用多个参数时用到&符号,因此未包含&符号 */ - public final static String SAFE_SCRIPT_PATTERN = + /*public final static String SAFE_SCRIPT_PATTERN = "(\\||;|\\$|'|\\'|0x0d|0x0a|\\%27|\\%3B" + "|<>|\\[\\]|\\(\\)|/|\"" + "|script|alert|svg|confirm|prompt|onload" + "|%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 = + "(||)"; + + // 完整的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; } + 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 */ 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 false; + }*/ + if (mark == null) return false; + return XSS_PATTERN.matcher(mark).find(); } /** diff --git a/bonus-framework/src/main/java/com/bonus/framework/interceptor/ParamSecureHandler.java b/bonus-framework/src/main/java/com/bonus/framework/interceptor/ParamSecureHandler.java index cdab677..8d60e22 100644 --- a/bonus-framework/src/main/java/com/bonus/framework/interceptor/ParamSecureHandler.java +++ b/bonus-framework/src/main/java/com/bonus/framework/interceptor/ParamSecureHandler.java @@ -4,6 +4,7 @@ import com.alibaba.fastjson2.JSON; import com.bonus.common.core.domain.AjaxResult; import com.bonus.common.utils.SafeUtil; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import org.springframework.web.servlet.AsyncHandlerInterceptor; @@ -69,7 +70,7 @@ public class ParamSecureHandler implements AsyncHandlerInterceptor { String params = request.getParameter("params"); boolean flag = XssCheck.xssCleanNew(params); if(!flag){ - returnJson(response, "输入值非法", 500); + returnJson(response, "输入值非法", 500,null); return false; } return true; @@ -81,10 +82,20 @@ public class ParamSecureHandler implements AsyncHandlerInterceptor { * 校验参数是否合法 */ if (!requestWrapper.isChecked()) { - log.error("输入值非法: queryString={}, body={}", + /*log.error("输入值非法: queryString={}, body={}", StringUtils.defaultString(requestWrapper.getQueryString(), "null"), - StringUtils.defaultString(requestWrapper.getReaderParam(), "null")); - returnJson(response, "输入值非法", 500); + StringUtils.defaultString(requestWrapper.getReaderParam(), "null"));*/ + + Map map = new HashMap<>(16); + List 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; } // System.err.println(JSON.toJSONString(request.getParameterMap())); @@ -95,7 +106,7 @@ public class ParamSecureHandler implements AsyncHandlerInterceptor { Map map = requestWrapper.getParameterMap(); boolean checkParameterMap = checkParameterMap(map, requestUrl); if (!checkParameterMap) { - returnJson(response, "输入值非法", 500); + returnJson(response, "输入值非法", 500,null); return false; } @@ -108,7 +119,7 @@ public class ParamSecureHandler implements AsyncHandlerInterceptor { if (!requestUrl.contains(uplFile) && !requestUrl.contains(upImage) && !requestUrl.contains(path)) { boolean checkReader = checkReader(readerParam, requestUrl); if (!checkReader) { - returnJson(response, "不安全参数", 500); + returnJson(response, "不安全参数", 500,null); 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(); PrintWriter writer = null; response.setCharacterEncoding("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); try { writer = response.getWriter(); diff --git a/bonus-framework/src/main/java/com/bonus/framework/interceptor/XssRequestWrapper.java b/bonus-framework/src/main/java/com/bonus/framework/interceptor/XssRequestWrapper.java index 4f29bd9..45f9f78 100644 --- a/bonus-framework/src/main/java/com/bonus/framework/interceptor/XssRequestWrapper.java +++ b/bonus-framework/src/main/java/com/bonus/framework/interceptor/XssRequestWrapper.java @@ -1,5 +1,6 @@ package com.bonus.framework.interceptor; +import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.json.JSONArray; import org.json.JSONException; @@ -13,10 +14,7 @@ import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.regex.Pattern; /** @@ -31,6 +29,9 @@ public class XssRequestWrapper extends HttpServletRequestWrapper { private String streamParam; private boolean checked; + // 新增:记录非法参数和攻击内容 + private List illegalParameters = new ArrayList<>(); + // 预编译所有正则表达式模式以提高性能 private static final List XSS_PATTERNS = new ArrayList<>(); @@ -114,7 +115,9 @@ public class XssRequestWrapper extends HttpServletRequestWrapper { // 检查安全性(仅校验,不篡改) 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) { log.error("读取请求体数据失败: {}", e.getLocalizedMessage(), e); @@ -124,7 +127,7 @@ public class XssRequestWrapper extends HttpServletRequestWrapper { } // 不对查询串做篡改,保持原样,防止影响参数绑定 - queryString = request.getQueryString(); + queryString = super.getQueryString(); } @Override @@ -173,7 +176,7 @@ public class XssRequestWrapper extends HttpServletRequestWrapper { String[] values = entry.getValue(); for (int i = 0; i < values.length; i++) { 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]; for (int i = 0; i < values.length; i++) { if (values[i] != null) { - cleanedValues[i] = xssClean(values[i]); + cleanedValues[i] = xssClean(paramString, values[i]); } } return cleanedValues; @@ -203,7 +206,7 @@ public class XssRequestWrapper extends HttpServletRequestWrapper { if (value == null) { return null; } - return xssClean(value); + return xssClean(paramString, value); } @Override @@ -212,22 +215,29 @@ public class XssRequestWrapper extends HttpServletRequestWrapper { if (value == 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) { return null; } + String cleanedValue = value; // 使用预编译的模式 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) { return true; } @@ -235,13 +245,15 @@ public class XssRequestWrapper extends HttpServletRequestWrapper { // 首先检查是否为JSON格式 if (isJsonString(value)) { // 对JSON字符串进行特殊处理:检查值部分是否包含XSS - return isJsonSafe(value); + return isJsonSafe(paramName, value); } // 使用预编译的模式进行检查 for (Pattern pattern : XSS_PATTERNS) { if (match(pattern, value)) { - return false; // 发现XSS攻击 + // 发现XSS攻击,记录非法参数 + recordIllegalParameter(paramName, value, null, "XSS_PATTERN_" + pattern.pattern()); + return false; } } return true; // 安全 @@ -274,91 +286,101 @@ public class XssRequestWrapper extends HttpServletRequestWrapper { } // 检查JSON字符串中的值是否安全 - private boolean isJsonSafe(String jsonStr) { + private boolean isJsonSafe(String paramName, String jsonStr) { try { // 处理JSON对象 if (jsonStr.trim().startsWith("{")) { JSONObject jsonObject = new JSONObject(jsonStr); - return isJsonObjectSafe(jsonObject); + return isJsonObjectSafe(paramName, jsonObject); } // 处理JSON数组 else if (jsonStr.trim().startsWith("[")) { JSONArray jsonArray = new JSONArray(jsonStr); - return isJsonArraySafe(jsonArray); + return isJsonArraySafe(paramName, jsonArray); } return true; } catch (JSONException e) { // 解析失败,回退到普通字符串检查 - return xssCleanNewFallback(jsonStr); + return xssCleanNewFallback(paramName, jsonStr); } } // 递归检查JSON对象的安全性 - private boolean isJsonObjectSafe(JSONObject jsonObject) throws JSONException { + private boolean isJsonObjectSafe(String parentKey, JSONObject jsonObject) throws JSONException { Iterator keys = jsonObject.keys(); + boolean isSafe = true; + while (keys.hasNext()) { String key = keys.next(); Object value = jsonObject.get(key); + String fullKey = parentKey + "." + key; + // 检查key的安全性 - if (!xssCleanNewFallback(key)) { - return false; + if (!xssCleanNewFallback(fullKey + "[KEY]", key)) { + isSafe = false; } // 检查value的安全性 if (value instanceof String) { - if (!xssCleanNewFallback((String) value)) { - return false; + if (!xssCleanNewFallback(fullKey, (String) value)) { + isSafe = false; } } else if (value instanceof JSONObject) { - if (!isJsonObjectSafe((JSONObject) value)) { - return false; + if (!isJsonObjectSafe(fullKey, (JSONObject) value)) { + isSafe = false; } } else if (value instanceof JSONArray) { - if (!isJsonArraySafe((JSONArray) value)) { - return false; + if (!isJsonArraySafe(fullKey, (JSONArray) value)) { + isSafe = false; } } // 其他类型(数字、布尔值等)视为安全 } - return true; + return isSafe; } // 递归检查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++) { Object value = jsonArray.get(i); + String fullKey = parentKey + "[" + i + "]"; if (value instanceof String) { - if (!xssCleanNewFallback((String) value)) { - return false; + if (!xssCleanNewFallback(fullKey, (String) value)) { + isSafe = false; } } else if (value instanceof JSONObject) { - if (!isJsonObjectSafe((JSONObject) value)) { - return false; + if (!isJsonObjectSafe(fullKey, (JSONObject) value)) { + isSafe = false; } } else if (value instanceof JSONArray) { - if (!isJsonArraySafe((JSONArray) value)) { - return false; + if (!isJsonArraySafe(fullKey, (JSONArray) value)) { + isSafe = false; } } // 其他类型(数字、布尔值等)视为安全 } - return true; + return isSafe; } // 回退到原始的模式匹配(避免递归调用) - private boolean xssCleanNewFallback(String value) { + private boolean xssCleanNewFallback(String paramName, String value) { if (value == null) { return true; } + boolean isSafe = true; for (Pattern pattern : XSS_PATTERNS) { 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(); } + /** + * 记录非法参数信息 + */ + 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() { return streamParam; } @@ -380,9 +442,44 @@ public class XssRequestWrapper extends HttpServletRequestWrapper { this.checked = checked; } + // 新增:获取非法参数列表 + public List 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() { String encoding = super.getCharacterEncoding(); 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 + + } } \ No newline at end of file