chanjs 2.0.3 → 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,
@@ -65,8 +71,8 @@ class Chan {
65
71
  const {
66
72
  views,
67
73
  env,
68
- appName,
69
- version,
74
+ APP_NAME,
75
+ APP_VERSION,
70
76
  cookieKey,
71
77
  BODY_LIMIT,
72
78
  statics,
@@ -81,7 +87,7 @@ class Chan {
81
87
  setTemplate(this.app, { views, env });
82
88
  setStatic(this.app, statics);
83
89
  Cors(this.app, cors);
84
- setHeader(this.app, { appName, version });
90
+ setHeader(this.app, { APP_NAME, APP_VERSION });
85
91
  }
86
92
 
87
93
  //数据库操作
@@ -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);
@@ -1,9 +1,9 @@
1
- export let setHeader = (app, { appName, version })=>{
1
+ export let setHeader = (app, { APP_NAME, APP_VERSION })=>{
2
2
  app.use((req, res, next) => {
3
3
  res.setHeader("Create-By", "Chanjs");
4
4
  res.setHeader("X-Powered-By", "ChanCMS");
5
- res.setHeader("ChanCMS", version);
6
- res.setHeader("Server", appName);
5
+ res.setHeader("ChanCMS", APP_VERSION);
6
+ res.setHeader("Server", APP_NAME);
7
7
  next();
8
8
  });
9
9
  };
@@ -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
+