package com.zsElectric.boot.common.util; import cn.hutool.core.util.StrUtil; import lombok.extern.slf4j.Slf4j; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import java.util.regex.Pattern; /** * 安全防护工具类 *

* 提供 XSS 攻击和 SQL 注入防护的核心检测方法 * * @author zsElectric */ @Slf4j public class SecurityUtils { /** * SQL 注入检测是否使用严格模式 * 默认为宽松模式以减少误判 */ private static volatile boolean sqlStrictMode = false; /** * 设置 SQL 注入检测模式 * * @param strictMode true 为严格模式,false 为宽松模式 */ public static void setSqlStrictMode(boolean strictMode) { sqlStrictMode = strictMode; } /** * XSS 攻击检测正则表达式 */ private static final Pattern[] XSS_PATTERNS = { // Script 标签 Pattern.compile("]*?>.*?", Pattern.CASE_INSENSITIVE | Pattern.DOTALL), Pattern.compile("]*?>", Pattern.CASE_INSENSITIVE), Pattern.compile("", Pattern.CASE_INSENSITIVE), // JavaScript 事件 Pattern.compile("javascript:", Pattern.CASE_INSENSITIVE), Pattern.compile("onerror\\s*=", Pattern.CASE_INSENSITIVE), Pattern.compile("onload\\s*=", Pattern.CASE_INSENSITIVE), Pattern.compile("onclick\\s*=", Pattern.CASE_INSENSITIVE), Pattern.compile("onmouseover\\s*=", Pattern.CASE_INSENSITIVE), Pattern.compile("onfocus\\s*=", Pattern.CASE_INSENSITIVE), Pattern.compile("onblur\\s*=", Pattern.CASE_INSENSITIVE), Pattern.compile("onsubmit\\s*=", Pattern.CASE_INSENSITIVE), Pattern.compile("onreset\\s*=", Pattern.CASE_INSENSITIVE), Pattern.compile("onselect\\s*=", Pattern.CASE_INSENSITIVE), Pattern.compile("onchange\\s*=", Pattern.CASE_INSENSITIVE), // iframe 标签 Pattern.compile("]*?>.*?", Pattern.CASE_INSENSITIVE | Pattern.DOTALL), Pattern.compile("]*?>", Pattern.CASE_INSENSITIVE), // embed、object 标签 Pattern.compile("]*?>", Pattern.CASE_INSENSITIVE), Pattern.compile("]*?>", Pattern.CASE_INSENSITIVE), // eval、expression Pattern.compile("eval\\s*\\(", Pattern.CASE_INSENSITIVE), Pattern.compile("expression\\s*\\(", Pattern.CASE_INSENSITIVE), // vbscript Pattern.compile("vbscript:", Pattern.CASE_INSENSITIVE), // img 标签的 src Pattern.compile("]+src[\\s]*=[\\s]*['\"]?javascript:", Pattern.CASE_INSENSITIVE), // style 中的 expression Pattern.compile("style\\s*=.*expression", Pattern.CASE_INSENSITIVE), // base64 编码的脚本 Pattern.compile("data:text/html;base64", Pattern.CASE_INSENSITIVE), // SVG Pattern.compile("]*?>.*?", Pattern.CASE_INSENSITIVE | Pattern.DOTALL), // Meta 标签 Pattern.compile("]*?>", Pattern.CASE_INSENSITIVE), // Link 标签 Pattern.compile("]*?>", Pattern.CASE_INSENSITIVE) }; /** * SQL 注入危险关键词 */ private static final Set SQL_KEYWORDS = new HashSet<>(Arrays.asList( // DML 语句 "select", "insert", "update", "delete", // DDL 语句 "drop", "create", "alter", "truncate", // DCL 语句 "grant", "revoke", // 联合查询 "union", "join", // 系统函数和存储过程 "exec", "execute", "xp_cmdshell", "sp_executesql", // 信息获取 "information_schema", "mysql.user", "sys.", // 条件判断 "case", "when", "then", "else", "end", // 其他危险操作 "declare", "cast", "convert", "char", "chr", "concat", "load_file", "into outfile", "into dumpfile", "benchmark", "sleep", "waitfor", "delay", // 子查询 "exists", "any", "all", "some" )); /** * SQL 注入检测正则表达式 */ private static final Pattern[] SQL_INJECTION_PATTERNS = { // SQL 注释 Pattern.compile("('.+--)|(--)|(;)|(\\|{2})"), // SQL 函数调用 Pattern.compile("\\bexec(ute)?\\s*\\(", Pattern.CASE_INSENSITIVE), // union 查询 Pattern.compile("\\bunion\\b.*\\bselect\\b", Pattern.CASE_INSENSITIVE), // 多语句 Pattern.compile(";.*?(select|insert|update|delete|drop|create|alter)", Pattern.CASE_INSENSITIVE), // 16 进制编码 Pattern.compile("0x[0-9a-f]+", Pattern.CASE_INSENSITIVE), // 字符串拼接 Pattern.compile("\\bconcat\\s*\\(", Pattern.CASE_INSENSITIVE), // sleep 函数 Pattern.compile("\\bsleep\\s*\\(", Pattern.CASE_INSENSITIVE), // benchmark 函数 Pattern.compile("\\bbenckmark\\s*\\(", Pattern.CASE_INSENSITIVE), // waitfor delay Pattern.compile("\\bwaitfor\\s+\\bdelay\\b", Pattern.CASE_INSENSITIVE), // 子查询 Pattern.compile("\\bsubstr\\s*\\(", Pattern.CASE_INSENSITIVE), Pattern.compile("\\bsubstring\\s*\\(", Pattern.CASE_INSENSITIVE) }; /** * 检测 XSS 攻击 * * @param value 待检测的字符串 * @return 如果检测到 XSS 攻击返回 true,否则返回 false */ public static boolean containsXss(String value) { if (StrUtil.isBlank(value)) { return false; } // 解码 URL 编码 String decodedValue = urlDecode(value); // 使用正则表达式检测 for (Pattern pattern : XSS_PATTERNS) { if (pattern.matcher(decodedValue).find()) { log.warn("检测到 XSS 攻击,匹配模式: {}, 内容: {}", pattern.pattern(), value); return true; } } return false; } /** * 检测 SQL 注入 * * @param value 待检测的字符串 * @return 如果检测到 SQL 注入返回 true,否则返回 false */ public static boolean containsSqlInjection(String value) { if (StrUtil.isBlank(value)) { return false; } String lowerValue = value.toLowerCase(); // 检查注释符号(更严格的检测) if (lowerValue.contains("--") || lowerValue.contains("/*") || lowerValue.contains("*/") || lowerValue.contains("#")) { // 检查是否是真正的注释而不是普通文本 if (lowerValue.matches(".*\\s(--|#).*") || lowerValue.contains("/*") || lowerValue.contains("*/")) { log.warn("检测到 SQL 注入注释符号: {}, 内容: {}", "--/#/*", value); return true; } } // 检查危险关键词(使用更精确的匹配规则) for (String keyword : SQL_KEYWORDS) { // 使用单词边界进行匹配,避免误判(例如:"selection" 不应匹配 "select") // 同时确保关键词前后不是字母数字字符 String pattern = "([^a-zA-Z0-9]|^)" + keyword + "([^a-zA-Z0-9]|$)"; if (Pattern.compile(pattern, Pattern.CASE_INSENSITIVE).matcher(lowerValue).find()) { // 进一步检查是否为真实攻击而非正常文本 // 例如:"select" 在 "selected" 中是正常文本,但在 "select * from" 中可能是攻击 if (isRealSqlInjection(keyword, lowerValue)) { log.warn("检测到 SQL 注入关键词: {}, 内容: {}", keyword, value); return true; } } } // 使用正则表达式检测 for (Pattern pattern : SQL_INJECTION_PATTERNS) { if (pattern.matcher(value).find()) { log.warn("检测到 SQL 注入,匹配模式: {}, 内容: {}", pattern.pattern(), value); return true; } } return false; } /** * 清理 XSS 攻击内容(转义特殊字符) * * @param value 待清理的字符串 * @return 清理后的字符串 */ public static String cleanXss(String value) { if (StrUtil.isBlank(value)) { return value; } // HTML 实体编码 value = value.replace("&", "&") .replace("<", "<") .replace(">", ">") .replace("\"", """) .replace("'", "'") .replace("/", "/"); return value; } /** * URL 解码(支持多次编码) * * @param value 待解码的字符串 * @return 解码后的字符串 */ private static String urlDecode(String value) { String decoded = value; try { // 最多解码 3 次,防止多重编码绕过 for (int i = 0; i < 3; i++) { String temp = java.net.URLDecoder.decode(decoded, "UTF-8"); if (temp.equals(decoded)) { break; } decoded = temp; } } catch (Exception e) { log.warn("URL 解码失败: {}", value, e); } return decoded; } /** * 验证输入是否安全(综合检查 XSS 和 SQL 注入) * * @param value 待验证的字符串 * @return 如果输入安全返回 true,否则返回 false */ public static boolean isSafeInput(String value) { return !containsXss(value) && !containsSqlInjection(value); } /** * 判断是否为真实的SQL注入攻击 * * @param keyword 检测到的关键词 * @param value 待检测的字符串(小写) * @return 如果是真实攻击返回 true,否则返回 false */ private static boolean isRealSqlInjection(String keyword, String value) { if (!sqlStrictMode) { // 在宽松模式下,只对明显的攻击模式进行拦截 switch (keyword) { case "select": // 只有当 select 后面跟着典型的 SQL 结构时才认为是攻击 boolean isSelectAttack = value.matches(".*select\\s+[a-z0-9_*]+\\s+from\\s+[a-z0-9_]+.*") || value.matches(".*select\\s+\\*\\s+from\\s+[a-z0-9_]+.*"); if (isSelectAttack) { log.debug("检测到可能的 SELECT 攻击: {}", value); } return isSelectAttack; case "insert": boolean isInsertAttack = value.contains("insert into"); if (isInsertAttack) { log.debug("检测到可能的 INSERT 攻击: {}", value); } return isInsertAttack; case "update": boolean isUpdateAttack = value.contains("update ") && value.contains(" set "); if (isUpdateAttack) { log.debug("检测到可能的 UPDATE 攻击: {}", value); } return isUpdateAttack; case "delete": boolean isDeleteAttack = value.contains("delete from"); if (isDeleteAttack) { log.debug("检测到可能的 DELETE 攻击: {}", value); } return isDeleteAttack; case "drop": case "create": case "alter": case "truncate": boolean isDdlAttack = value.contains(keyword + " "); if (isDdlAttack) { log.debug("检测到可能的 DDL 攻击 ({}): {}", keyword, value); } return isDdlAttack; case "union": boolean isUnionAttack = value.contains("union select"); if (isUnionAttack) { log.debug("检测到可能的 UNION 攻击: {}", value); } return isUnionAttack; case "exec": case "execute": boolean isExecAttack = value.contains(keyword + "("); if (isExecAttack) { log.debug("检测到可能的 EXEC 攻击 ({}): {}", keyword, value); } return isExecAttack; default: // 对于其他关键词,采用宽松策略,避免误判正常用户名等 return false; } } else { // 严格模式下保持原来的逻辑 switch (keyword) { case "select": // select 通常是攻击的一部分,后面跟着列名和 from boolean isSelectAttack = value.matches(".*select\\s+[a-z0-9_*]+\\s+from\\s+[a-z0-9_]+.*") || value.matches(".*select\\s+\\*\\s+from\\s+[a-z0-9_]+.*"); if (isSelectAttack) { log.debug("[严格模式] 检测到可能的 SELECT 攻击: {}", value); } return isSelectAttack; case "insert": // insert 通常是攻击的一部分,后面跟着 into boolean isInsertAttack = value.contains("insert into"); if (isInsertAttack) { log.debug("[严格模式] 检测到可能的 INSERT 攻击: {}", value); } return isInsertAttack; case "update": // update 通常是攻击的一部分,后面跟着 set boolean isUpdateAttack = value.contains("update ") && value.contains(" set "); if (isUpdateAttack) { log.debug("[严格模式] 检测到可能的 UPDATE 攻击: {}", value); } return isUpdateAttack; case "delete": // delete 通常是攻击的一部分,后面跟着 from boolean isDeleteAttack = value.contains("delete from"); if (isDeleteAttack) { log.debug("[严格模式] 检测到可能的 DELETE 攻击: {}", value); } return isDeleteAttack; case "drop": case "create": case "alter": case "truncate": // DDL 语句通常是攻击的一部分 boolean isDdlAttack = value.contains(keyword + " "); if (isDdlAttack) { log.debug("[严格模式] 检测到可能的 DDL 攻击 ({}): {}", keyword, value); } return isDdlAttack; case "union": // union 通常是攻击的一部分,后面跟着 select boolean isUnionAttack = value.contains("union select"); if (isUnionAttack) { log.debug("[严格模式] 检测到可能的 UNION 攻击: {}", value); } return isUnionAttack; default: // 对于其他关键词,采用宽松策略,避免误判正常用户名等 return false; } } } }