chanjs 2.0.4 → 2.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/helper/safe.js ADDED
@@ -0,0 +1,263 @@
1
+ /**
2
+ * 安全中间件:用于防御常见Web攻击,包括XSS、SQL注入、点击劫持等
3
+ * 功能:1. 检测URL中的敏感关键词 2. 设置X-Frame-Options头防止点击劫持
4
+ */
5
+ /**
6
+ * 扩展后的敏感关键词列表
7
+ * 包含:敏感文件扩展名、路径遍历、管理路径、SQL注入、XSS、命令注入等常见攻击特征
8
+ */
9
+ const defaultKeywords = [
10
+ // 敏感文件扩展名(路径遍历/敏感文件访问)
11
+ "\\.(php|asp|aspx|jsp|jspx|go|cgi|pl|sh|bat|exe|sql|rar|gz|tar|tgz|7z|json|xml|vscode|bak|well-known|log|conf|ini|env|yml|yaml|pem|key|crt|db|sqlite)",
12
+
13
+ // 路径遍历特征(目录跳转)
14
+ "\\.\\./", // ../ 上级目录
15
+ "\\.\\.\\\\", // ..\ Windows系统上级目录
16
+ "/etc/", // 系统配置目录
17
+ "/proc/", // 进程信息目录
18
+ "/dev/", // 设备文件目录
19
+ "/root/", // 根用户目录
20
+ "/home/", // 用户主目录
21
+ "C:\\\\Windows\\\\", // Windows系统目录
22
+ "C:\\\\Users\\\\", // Windows用户目录
23
+
24
+ // 敏感管理路径
25
+ "/manager/",
26
+ "/backend/",
27
+ "/system/",
28
+ "/control/",
29
+ "/dashboard/",
30
+ "/wp-admin/", // WordPress管理后台
31
+ "/phpmyadmin/", // MySQL管理工具
32
+ "/adminer/",
33
+ "/pma/",
34
+
35
+ // SQL注入相关关键词
36
+ "union\\s+select",
37
+ "drop\\s+table",
38
+ "delete\\s+from",
39
+ "insert\\s+into",
40
+ "update\\s+set",
41
+ "exec\\s+\\(",
42
+ "xp_cmdshell", // SQL Server命令执行
43
+ "into\\s+outfile", // MySQL写文件
44
+ "load_file\\(", // MySQL读文件
45
+ "where\\s+1=1", // 条件注入
46
+ "or\\s+1=1",
47
+
48
+ // XSS相关关键词
49
+ "script",
50
+ "alert",
51
+ "iframe",
52
+ "onerror",
53
+ "onclick",
54
+ "onload",
55
+ "onmouseover",
56
+ "confirm",
57
+ "prompt",
58
+ "eval",
59
+ // "javascript:", // JS伪协议
60
+ // "vbscript:", // VBScript伪协议
61
+ // "<img", // 图片标签注入
62
+ // "<svg", // SVG注入
63
+ // "<video",
64
+ // "<audio",
65
+
66
+ // 命令注入相关
67
+ "cmd\\.exe",
68
+ "bash",
69
+ "sh",
70
+ "powershell",
71
+ "cmd",
72
+ "system\\(",
73
+ "shell_exec\\(",
74
+ "exec\\(",
75
+ "passthru\\(",
76
+ "`.*`", // 反引号命令执行
77
+ "\\|", // 管道符
78
+ "&", // 后台执行符
79
+ ";", // 命令分隔符
80
+
81
+ // 敏感操作/信息
82
+ "document\\.cookie",
83
+ "window\\.location",
84
+ "fetch",
85
+ "xhr",
86
+ "ajax",
87
+ "data:", // data协议
88
+ "root", // 管理员用户
89
+ "backup",
90
+ "callback",
91
+ "ftp",
92
+ "ssh",
93
+ "telnet",
94
+
95
+ "token",
96
+ "secret",
97
+ "password",
98
+ "passwd",
99
+
100
+ ];
101
+
102
+ export default defaultKeywords;
103
+
104
+
105
+
106
+ /**
107
+ * 安全中间件:简化配置的Web安全防御工具
108
+ * 功能:防点击劫持、敏感请求拦截
109
+ */
110
+
111
+ // 基础敏感关键词(高风险攻击特征)
112
+ const baseKeywords = [
113
+ // SQL注入核心特征
114
+ 'union select', 'drop table', 'delete from',
115
+ 'insert into', 'update .+ set', 'exec\\(',
116
+ // XSS核心特征
117
+ '<script', 'onerror=', 'onclick=',
118
+ 'eval\\(', 'document\\.cookie'
119
+ ];
120
+
121
+ /**
122
+ * 安全中间件主函数
123
+ * @param {Object} options - 配置选项
124
+ * @param {string[]} [options.keywords=[]] - 自定义敏感关键词(会与基础关键词合并)
125
+ * @param {number} [options.threshold=1] - 敏感词匹配阈值(默认匹配1个即拦截)
126
+ * @param {string} [options.framePolicy='SAMEORIGIN'] - X-Frame-Options策略:DENY/SAMEORIGIN/ALLOW-FROM
127
+ * @param {string} [options.allowFrom] - 当framePolicy为ALLOW-FROM时的允许地址
128
+ * @param {string} [options.blockMessage='请求被安全策略拦截'] - 拦截提示信息
129
+ * @param {Function} [options.onBlock] - 拦截时的回调函数
130
+ * @returns {Function} Express中间件函数
131
+ */
132
+ export const safe = (options = {}) => {
133
+ // 合并默认配置
134
+ const {
135
+ keywords = [],
136
+ threshold = 1,
137
+ framePolicy = 'SAMEORIGIN',
138
+ allowFrom = '',
139
+ blockMessage = '请求被安全策略拦截',
140
+ onBlock = () => {}
141
+ } = options;
142
+
143
+ // 处理关键词:转义特殊字符,合并基础关键词与自定义关键词
144
+ const escapedKeywords = [
145
+ ...baseKeywords,
146
+ ...keywords.map(kw => kw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
147
+ ];
148
+
149
+ // 生成不区分大小写的全局匹配正则
150
+ const keywordReg = new RegExp(`(${escapedKeywords.join('|')})`, 'ig');
151
+
152
+ return (req, res, next) => {
153
+ // 1. 设置防点击劫持头
154
+ let frameHeader = 'SAMEORIGIN';
155
+ if (framePolicy === 'DENY') {
156
+ frameHeader = 'DENY';
157
+ } else if (framePolicy === 'ALLOW-FROM' && allowFrom) {
158
+ frameHeader = `ALLOW-FROM ${allowFrom}`;
159
+ }
160
+ res.setHeader('X-Frame-Options', frameHeader);
161
+
162
+ // 2. 添加XSS防御头
163
+ res.setHeader('X-XSS-Protection', '1; mode=block');
164
+
165
+ // 3. 敏感请求检查
166
+ // 拼接URL和查询参数作为检查文本
167
+ const checkText = `${req.url} ${JSON.stringify(req.query)}`;
168
+ // 匹配所有出现的敏感词
169
+ const matches = checkText.match(keywordReg) || [];
170
+
171
+ // 去重后判断是否达到阈值
172
+ if ([...new Set(matches)].length >= threshold) {
173
+ const logInfo = {
174
+ method: req.method,
175
+ url: req.url,
176
+ ip: req.ip,
177
+ matches: [...new Set(matches)]
178
+ };
179
+ console.error('[安全拦截] 疑似恶意请求:', logInfo);
180
+ onBlock(logInfo); // 触发拦截回调
181
+ return res.status(403).send(blockMessage);
182
+ }
183
+
184
+ // 检查通过,继续处理
185
+ next();
186
+ };
187
+ };
188
+
189
+
190
+
191
+ // utils/safe.js
192
+ const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
193
+
194
+ const BASE_KEYWORDS = [
195
+ 'union select', 'drop table', 'delete from',
196
+ 'insert into', 'exec(', '<script', 'onerror=',
197
+ 'onclick=', 'eval(', 'document.cookie'
198
+ ];
199
+
200
+ export const safe = (options = {}) => {
201
+ const {
202
+ keywords = [],
203
+ threshold = 1,
204
+ blockMessage = '请求被安全策略拦截',
205
+ onBlock = () => {},
206
+ enableBodyCheck = true,
207
+ ignorePaths = [/\/static\//, /\/assets\//, /\.(js|css|png|jpg|jpeg|gif|ico)$/i]
208
+ } = options;
209
+
210
+ // 白名单路径检查
211
+ const isIgnoredPath = (url) => {
212
+ return ignorePaths.some(pattern => pattern.test(url));
213
+ };
214
+
215
+ // 所有关键词转义
216
+ const keywordList = [...BASE_KEYWORDS, ...keywords].map(escapeRegExp);
217
+
218
+ return (req, res, next) => {
219
+ // 1. 白名单路径跳过检查
220
+ if (isIgnoredPath(req.url)) {
221
+ return next();
222
+ }
223
+
224
+ // 2. 设置安全头
225
+ res.setHeader('X-Frame-Options', 'DENY');
226
+ res.setHeader('X-Content-Type-Options', 'nosniff');
227
+ res.setHeader('X-XSS-Protection', '1; mode=block');
228
+ res.setHeader('Referrer-Policy', 'no-referrer-when-downgrade');
229
+ res.setHeader('Content-Security-Policy', "frame-ancestors 'self'; default-src 'self'");
230
+
231
+ // 3. 构造检查文本
232
+ let checkText = `${req.url} ${JSON.stringify(req.query)}`;
233
+ if (enableBodyCheck && req.body) {
234
+ checkText += ` ${JSON.stringify(req.body)}`;
235
+ }
236
+ checkText = checkText.toLowerCase();
237
+
238
+ // 4. 逐个检查关键词(性能好,可提前退出)
239
+ const matches = [];
240
+ for (const kw of keywordList) {
241
+ if (checkText.includes(kw.toLowerCase())) {
242
+ matches.push(kw);
243
+ if (matches.length >= threshold) break;
244
+ }
245
+ }
246
+
247
+ if (matches.length >= threshold) {
248
+ const logInfo = {
249
+ method: req.method,
250
+ url: req.url,
251
+ ip: req.ip,
252
+ userAgent: req.get('User-Agent'),
253
+ matches: [...new Set(matches)],
254
+ timestamp: new Date().toISOString()
255
+ };
256
+ console.warn('[安全拦截] 疑似恶意请求:', logInfo);
257
+ onBlock(logInfo);
258
+ return res.status(403).send(blockMessage);
259
+ }
260
+
261
+ next();
262
+ };
263
+ };
package/helper/time.js ADDED
@@ -0,0 +1,28 @@
1
+ import dayjs from "dayjs";
2
+ import "dayjs/locale/zh-cn.js";
3
+ import relativeTime from "dayjs/plugin/relativeTime.js";
4
+
5
+ dayjs.extend(relativeTime);
6
+ dayjs.locale("zh-cn");
7
+
8
+ /**
9
+ * @description 格式化时间
10
+ * @param {Array} data 数组
11
+ * @param {Boolean} time 是否开启具体时间
12
+ * @param {String} format YYYY-MM-DD HH:mm:ss
13
+ * @returns 返回处理过的数组
14
+ */
15
+ export const formatDay = (data, time = true, format = "YYYY-MM-DD") => {
16
+ data.forEach((item) => {
17
+ if (item.createdAt) {
18
+ item.createdAt = time
19
+ ? dayjs(item.createdAt).format(format)
20
+ : dayjs(item.createdAt).fromNow().replace(" ", "");
21
+ }
22
+ });
23
+ return data;
24
+ }
25
+
26
+ export const formatTime = (data, format = "YYYY-MM-DD HH:mm:ss") => {
27
+ return dayjs(data).format("YYYY-MM-DD HH:mm:ss");
28
+ }
package/helper/tree.js ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @param {Array} arr - 原始数据数组
3
+ * @param {number|string} [pid=0] - 根节点父ID
4
+ * @param {string} [idKey='id'] - ID字段名
5
+ * @param {string} [pidKey='pid'] - 父ID字段名
6
+ * @param {string} [childrenKey='children'] - 子节点字段名
7
+ * @returns {Array} 树形结构数组
8
+ */
9
+ export function buildTree(arr, pid = 0, idKey = 'id', pidKey = 'pid', childrenKey = 'children') {
10
+ // 基础参数校验
11
+ if (!Array.isArray(arr)) return [];
12
+
13
+ const tree = [];
14
+
15
+ for (let i = 0; i < arr.length; i++) {
16
+ const item = arr[i];
17
+ // 找到当前层级的节点
18
+ if (item[pidKey] === pid) {
19
+ // 递归查找子节点(通过slice创建子数组,避免重复遍历已处理项)
20
+ const children = buildTree(arr.slice(i + 1), item[idKey], idKey, pidKey, childrenKey);
21
+
22
+ // 有子节点则添加,避免空数组
23
+ if (children.length) {
24
+ item[childrenKey] = children;
25
+ }
26
+
27
+ tree.push(item);
28
+ }
29
+ }
30
+
31
+ return tree;
32
+ }
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import "./common/global.js";
1
+ import "./global/index.js";
2
2
  import path from "path";
3
3
  import fs from "fs";
4
4
  import {
@@ -24,6 +24,12 @@ import {
24
24
  } from "./middleware/index.js";
25
25
 
26
26
  class Chan {
27
+
28
+ //版本号
29
+ #version = "0.0.0";
30
+
31
+
32
+
27
33
  static helper = {
28
34
  bindClass,
29
35
  getPackage,
@@ -102,7 +108,7 @@ class Chan {
102
108
  if (fs.existsSync(configPath)) {
103
109
  const dirs = loaderSort(Chan.config.modules);
104
110
  for (const item of dirs) {
105
- let router = await importRootFile(`app/modules/${item}/router.js`);
111
+ let router = await importFile(`app/modules/${item}/router.js`);
106
112
  router(this.app, this.router, Chan.config);
107
113
  }
108
114
  }
@@ -111,7 +117,7 @@ class Chan {
111
117
  //通用路由,加载错误处理和500路由和爬虫处理
112
118
  async loadCommonRouter() {
113
119
  try {
114
- let router = await importRootFile("app/router.js");
120
+ let router = await importFile("app/router.js");
115
121
  router(this.app, this.router, Chan.config);
116
122
  } catch (error) {
117
123
  console.log(error);
@@ -0,0 +1,144 @@
1
+ /**
2
+ * 安全中间件:用于防御常见Web攻击,包括XSS、SQL注入、点击劫持等
3
+ * 功能:1. 检测URL中的敏感关键词 2. 设置X-Frame-Options头防止点击劫持
4
+ */
5
+
6
+ // 默认敏感关键词列表
7
+ // 包含:敏感文件扩展名、管理路径、SQL注入语句、XSS相关函数、敏感操作等
8
+ const defaultKeywords = [
9
+ // 敏感文件扩展名(可能用于路径遍历或敏感文件访问)
10
+ "\\.(php|asp|aspx|jsp|jspx|cgi|pl|sh|bat|exe|sql|rar|gz|tar|tgz|7z|json|xml|vscode|bak|well-known)",
11
+ "admin", // 管理后台路径
12
+ // SQL注入相关关键词
13
+ "union\\s+select",
14
+ "drop\\s+table",
15
+ "delete\\s+from",
16
+ "insert\\s+into",
17
+ "update\\s+set",
18
+ "exec\\s+\\(",
19
+ // XSS相关关键词
20
+ "script",
21
+ "alert",
22
+ "iframe",
23
+ "onerror",
24
+ "confirm",
25
+ "prompt",
26
+ "eval",
27
+ // 敏感操作/信息
28
+ "document\\.cookie",
29
+ "window\\.location",
30
+ "fetch",
31
+ "xhr",
32
+ "ajax",
33
+ "data",
34
+ "root",
35
+ "backup",
36
+ "callback",
37
+ "ftp"
38
+ ];
39
+
40
+ /**
41
+ * 关键词转义处理:将正则特殊字符转义,避免干扰正则匹配
42
+ * @param {string} keyword - 需要转义的关键词
43
+ * @returns {string} 转义后的关键词
44
+ */
45
+ const escapeRegExp = (keyword) => {
46
+ return keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
47
+ };
48
+
49
+ /**
50
+ * 检查URL中是否包含敏感关键词
51
+ * @param {string} url - 需要检查的URL
52
+ * @param {string[]} customKeywords - 自定义关键词列表
53
+ * @returns {boolean} 包含敏感词返回true,否则返回false
54
+ */
55
+ const checkKeywords = (url, customKeywords) => {
56
+ // 合并默认关键词和自定义关键词,并进行转义处理
57
+ const allKeywords = [
58
+ ...defaultKeywords.map(escapeRegExp),
59
+ ...customKeywords.map(escapeRegExp)
60
+ ];
61
+ // 创建不区分大小写的正则表达式
62
+ const keywordReg = new RegExp(allKeywords.join("|"), "i");
63
+ return keywordReg.test(url);
64
+ };
65
+
66
+ /**
67
+ * 设置X-Frame-Options响应头,防止点击劫持攻击
68
+ * @param {Object} res - Express响应对象
69
+ * @param {string} frameOption - 可选值:DENY/SAMEORIGIN/ALLOW-FROM
70
+ * @param {string} [allowFromUrl] - 当frameOption为ALLOW-FROM时的允许地址
71
+ */
72
+ const setXFrameOptions = (res, frameOption, allowFromUrl) => {
73
+ const options = {
74
+ DENY: "DENY", // 禁止任何页面嵌入
75
+ SAMEORIGIN: "SAMEORIGIN", // 只允许同源页面嵌入
76
+ "ALLOW-FROM": `ALLOW-FROM ${allowFromUrl || ''}` // 允许指定来源嵌入
77
+ };
78
+
79
+ // 验证参数有效性
80
+ if (!Object.keys(options).includes(frameOption)) {
81
+ console.warn(`无效的X-Frame-Options配置: ${frameOption},已自动使用SAMEORIGIN`);
82
+ res.set("X-Frame-Options", "SAMEORIGIN");
83
+ return;
84
+ }
85
+
86
+ // 处理ALLOW-FROM的特殊情况
87
+ if (frameOption === "ALLOW-FROM" && !allowFromUrl) {
88
+ console.warn("使用ALLOW-FROM时必须指定allowFromUrl,已自动使用SAMEORIGIN");
89
+ res.set("X-Frame-Options", "SAMEORIGIN");
90
+ return;
91
+ }
92
+
93
+ res.set("X-Frame-Options", options[frameOption]);
94
+ };
95
+
96
+ /**
97
+ * 安全中间件主函数
98
+ * @param {Object} options - 配置选项
99
+ * @param {string[]} [options.keywords=[]] - 自定义敏感关键词列表
100
+ * @param {string} [options.sendText="未知请求,已阻止..."] - 拦截时返回的文本
101
+ * @param {string} [options.frameOption="SAMEORIGIN"] - X-Frame-Options配置
102
+ * @param {string} [options.allowFromUrl] - 当frameOption为ALLOW-FROM时的允许地址
103
+ * @returns {Function} Express中间件函数
104
+ * @example
105
+ app.use(safe({
106
+ keywords: ['custom-badword'], // 自定义敏感词
107
+ frameOption: 'DENY', // 禁止任何页面嵌入
108
+ sendText: '请求被安全策略拦截'
109
+ }));
110
+ */
111
+ export const safe = (options = {}) => {
112
+ // 合并默认配置和用户配置
113
+ const params = {
114
+ keywords: [],
115
+ sendText: "未知请求,已阻止...",
116
+ frameOption: "SAMEORIGIN",
117
+ allowFromUrl: "",
118
+ ...options
119
+ };
120
+
121
+ // 预编译关键词正则(优化性能:只编译一次)
122
+ const allKeywords = [
123
+ ...defaultKeywords.map(escapeRegExp),
124
+ ...params.keywords.map(escapeRegExp)
125
+ ];
126
+ const keywordReg = new RegExp(allKeywords.join("|"), "i");
127
+
128
+ // 返回中间件函数
129
+ return (req, res, next) => {
130
+ // 1. 设置X-Frame-Options头防止点击劫持
131
+ setXFrameOptions(res, params.frameOption, params.allowFromUrl);
132
+
133
+ // 2. 检查URL中是否包含敏感关键词
134
+ if (keywordReg.test(req.url)) {
135
+ console.error(`[安全拦截] 疑似恶意请求: ${req.method} ${req.url} 来自IP: ${req.ip}`);
136
+ return res.status(403).send(params.sendText);
137
+ }
138
+
139
+ // 检查通过,继续处理请求
140
+ next();
141
+ };
142
+ };
143
+
144
+
@@ -0,0 +1,197 @@
1
+ import url from "url";
2
+ import {getIp} from "../helper/ip.js";
3
+
4
+ // 原始关键词列表(保持不变)
5
+ const keywords = [
6
+ ".aspx",
7
+ ".php",
8
+ ".pl",
9
+ ".jsa",
10
+ ".jsp",
11
+ ".asp",
12
+ ".go",
13
+ ".jhtml",
14
+ ".shtml",
15
+ ".cfm",
16
+ ".cgi",
17
+ ".svn",
18
+ ".env",
19
+ ".keys",
20
+ ".cache",
21
+ ".hidden",
22
+ ".bod",
23
+ ".ll",
24
+ ".backup",
25
+ ".json",
26
+ ".xml",
27
+ ".bak",
28
+ ".aws",
29
+ ".database",
30
+ ".cookie",
31
+ ".location",
32
+ ".dump",
33
+ ".ftp",
34
+ ".idea",
35
+ ".s3",
36
+ ".sh",
37
+ ".old",
38
+ ".tf",
39
+ ".sql",
40
+ ".vscode",
41
+ ".docker",
42
+ ".map",
43
+ "1+1",
44
+ ".save",
45
+ ".gz",
46
+ ".yml",
47
+ ".tar",
48
+ ".rar",
49
+ ".7z",
50
+ ".zip",
51
+ ".git",
52
+ ".log",
53
+ ".local",
54
+ "../",
55
+ "db_",
56
+ "smtp",
57
+ "meta",
58
+ "debug",
59
+ "secret",
60
+ "/xampp/",
61
+ "/metadata/",
62
+ "/internal/",
63
+ "/aws/",
64
+ "/debug/",
65
+ "/configs/",
66
+ "/cgi-bin/",
67
+ "/tmp/",
68
+ "/staging/",
69
+ "/mail/",
70
+ "/docker/",
71
+ "/.secure/",
72
+ "/php-cgi/",
73
+ "/wp-",
74
+ "/backup/",
75
+ "password",
76
+ "redirect",
77
+ "/phpMyAdmin/",
78
+ "/setup/",
79
+ "concat(",
80
+ "version(",
81
+ "sleep(",
82
+ "benchmark(",
83
+ "0x7e",
84
+ "extractvalue(",
85
+ "(select",
86
+ "a%",
87
+ "union",
88
+ "drop",
89
+ "update",
90
+ "insert",
91
+ "delete",
92
+ "alter",
93
+ "truncate",
94
+ "create",
95
+ "exec",
96
+ ];
97
+
98
+ // 预处理:合并字符串关键词和正则关键词为两个单一正则(核心优化)
99
+ const { combinedStrRegex, combinedRegRegex } = (() => {
100
+ const regexSpecialChars = /[.*+?^${}()|[\]\\]/g;
101
+ const strKwParts = []; // 存储无特殊字符的关键词(用于合并正则)
102
+ const regKwParts = []; // 存储转义后的正则关键词(用于合并正则)
103
+
104
+ keywords.forEach((keyword) => {
105
+ if (regexSpecialChars.test(keyword)) {
106
+ // 含正则特殊字符:转义后加入正则关键词部分
107
+ const escaped = keyword.replace(regexSpecialChars, "\\$&");
108
+ regKwParts.push(escaped);
109
+ } else {
110
+ // 无特殊字符:直接加入字符串关键词部分
111
+ strKwParts.push(keyword);
112
+ }
113
+ });
114
+
115
+ // 构建合并后的正则(空数组时返回匹配失败的正则,避免报错)
116
+ const buildCombinedRegex = (parts) => {
117
+ return parts.length
118
+ ? new RegExp(`(?:${parts.join("|")})`, "i") // 非捕获组+不区分大小写
119
+ : new RegExp("^$"); // 匹配空字符串(永远不命中)
120
+ };
121
+
122
+ return {
123
+ combinedStrRegex: buildCombinedRegex(strKwParts),
124
+ combinedRegRegex: buildCombinedRegex(regKwParts),
125
+ };
126
+ })();
127
+
128
+ const safe = (req, res, next) => {
129
+ try {
130
+ // 1. 设置安全头(保持不变)
131
+ res.setHeader("X-Frame-Options", "SAMEORIGIN");
132
+ res.setHeader("X-Content-Type-Options", "nosniff");
133
+ res.setHeader("Referrer-Policy", "no-referrer-when-downgrade");
134
+ res.removeHeader("Server");
135
+
136
+ // 2. 构建检查文本:req.path(仅路径)+ query(非空才加)(优化冗余)
137
+ let checkText = req.path || "";
138
+ if (req.query && Object.keys(req.query).length > 0) {
139
+ const queryStr = Object.entries(req.query)
140
+ .map(([k, v]) => `${k}=${v}`)
141
+ .join(' ');
142
+ checkText += ` ${queryStr}`;
143
+ }
144
+
145
+ // 3. 处理请求体(优化序列化逻辑)
146
+ let bodyText = "";
147
+ const contentType = req.headers["content-type"] || "";
148
+ const isMultipart = contentType.includes("multipart/form-data");
149
+
150
+ if (!isMultipart && req.body) {
151
+ try {
152
+ // 若已是字符串直接用,否则序列化(避免重复序列化)
153
+ const bodyStr =
154
+ typeof req.body === "string" ? req.body : JSON.stringify(req.body);
155
+
156
+ // 限制大小(保持原逻辑,避免大文本开销)
157
+ if (bodyStr.length < 10000) {
158
+ bodyText = ` ${bodyStr}`;
159
+ }
160
+ } catch (e) {
161
+ // 忽略序列化错误
162
+ }
163
+ }
164
+
165
+ // 合并完整文本(无需 toLowerCase)
166
+ const fullText = checkText + bodyText;
167
+
168
+ // 4. 高效匹配:两次正则 test 替代 N 次循环(核心优化)
169
+ let foundMatch = false;
170
+ // 先检查字符串关键词合并正则(更快,因为无复杂正则逻辑)
171
+ if (combinedStrRegex.test(fullText)) {
172
+ foundMatch = true;
173
+ }
174
+ // 再检查正则关键词合并正则(仅当字符串匹配未命中时)
175
+ else if (combinedRegRegex.test(fullText)) {
176
+ foundMatch = true;
177
+ }
178
+
179
+ if (foundMatch) {
180
+ console.error("[安全拦截] 疑似恶意请求:", {
181
+ url: req.url,
182
+ ip: getIp(req),
183
+ userAgent: req.get("User-Agent") || "",
184
+ });
185
+ return res.status(403).send("请求被安全策略拦截");
186
+ }
187
+
188
+ next();
189
+ } catch (error) {
190
+ console.error("[安全中间件异常]", error);
191
+ res.status(500).send("服务器内部错误");
192
+ }
193
+ };
194
+
195
+ export const waf = (app) => {
196
+ app.use(safe);
197
+ };