|
|
@@ -0,0 +1,380 @@
|
|
|
+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;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 安全防护工具类
|
|
|
+ * <p>
|
|
|
+ * 提供 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("<script[^>]*?>.*?</script>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL),
|
|
|
+ Pattern.compile("<script[^>]*?>", Pattern.CASE_INSENSITIVE),
|
|
|
+ Pattern.compile("</script>", 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("<iframe[^>]*?>.*?</iframe>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL),
|
|
|
+ Pattern.compile("<iframe[^>]*?>", Pattern.CASE_INSENSITIVE),
|
|
|
+ // embed、object 标签
|
|
|
+ Pattern.compile("<embed[^>]*?>", Pattern.CASE_INSENSITIVE),
|
|
|
+ Pattern.compile("<object[^>]*?>", 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("<img[^>]+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("<svg[^>]*?>.*?</svg>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL),
|
|
|
+ // Meta 标签
|
|
|
+ Pattern.compile("<meta[^>]*?>", Pattern.CASE_INSENSITIVE),
|
|
|
+ // Link 标签
|
|
|
+ Pattern.compile("<link[^>]*?>", Pattern.CASE_INSENSITIVE)
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * SQL 注入危险关键词
|
|
|
+ */
|
|
|
+ private static final Set<String> 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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|