chanjs 2.7.2 → 2.7.3

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.
Files changed (67) hide show
  1. package/App.js +232 -16
  2. package/base/Aop.js +20 -3
  3. package/base/Container.js +80 -3
  4. package/base/Controller.js +38 -9
  5. package/base/Database.js +50 -0
  6. package/base/Event.js +12 -0
  7. package/base/{Service.js → Repository.js} +644 -539
  8. package/common/api.js +18 -8
  9. package/common/code.js +25 -15
  10. package/common/email.js +98 -17
  11. package/common/index.js +1 -1
  12. package/config/code.js +138 -82
  13. package/global/index.js +1 -1
  14. package/helper/index.js +43 -41
  15. package/index.js +19 -6
  16. package/loader/index.js +6 -0
  17. package/{helper → loader}/loader.js +41 -27
  18. package/middleware/compress.js +185 -0
  19. package/middleware/cors.js +36 -24
  20. package/middleware/header.js +5 -10
  21. package/middleware/index.js +1 -0
  22. package/middleware/log.js +27 -3
  23. package/middleware/setBody.js +9 -1
  24. package/middleware/static.js +2 -1
  25. package/middleware/template.js +139 -4
  26. package/middleware/waf.js +136 -76
  27. package/package.json +2 -3
  28. package/realtime/index.js +7 -0
  29. package/realtime/sse.js +424 -0
  30. package/realtime/websocket.js +540 -0
  31. package/response/index.js +12 -0
  32. package/response/response.js +258 -0
  33. package/schedule/index.js +6 -0
  34. package/schedule/schedule.js +491 -0
  35. package/{helper → security}/checker.js +23 -8
  36. package/security/index.js +14 -0
  37. package/{helper → security}/jwt.js +175 -107
  38. package/security/keywords.js +179 -0
  39. package/security/rate-limit.js +105 -0
  40. package/security/sign.js +210 -0
  41. package/security/xss-filter.js +63 -0
  42. package/storage/cache.js +258 -0
  43. package/storage/index.js +9 -0
  44. package/storage/redis.js +258 -0
  45. package/storage/store.js +266 -0
  46. package/{helper → utils}/file.js +106 -15
  47. package/{helper → utils}/filter.js +2 -1
  48. package/{helper → utils}/html.js +19 -1
  49. package/utils/index.js +34 -0
  50. package/{helper → utils}/ip.js +25 -16
  51. package/utils/request.js +172 -0
  52. package/{helper → utils}/time.js +1 -1
  53. package/utils/tree.js +121 -0
  54. package/common/category.js +0 -22
  55. package/common/sms.js +0 -104
  56. package/extend/art-template.js +0 -129
  57. package/extend/index.js +0 -6
  58. package/global/global.js +0 -63
  59. package/helper/cache.js +0 -187
  60. package/helper/keywords.js +0 -132
  61. package/helper/rate-limit.js +0 -116
  62. package/helper/request.js +0 -47
  63. package/helper/response.js +0 -180
  64. package/helper/sign.js +0 -96
  65. package/helper/tree.js +0 -77
  66. package/helper/xss-filter.js +0 -42
  67. /package/{helper → utils}/data-parse.js +0 -0
package/middleware/waf.js CHANGED
@@ -1,32 +1,28 @@
1
- import { getIp } from "../helper/ip.js";
2
- import { checkKeywords } from "../helper/checker.js";
3
- import { filterXSS } from "../helper/xss-filter.js";
4
- import { createRateLimitMiddleware } from "../helper/rate-limit.js";
5
-
6
- function parseCookies(cookieHeader) {
7
- if (!cookieHeader) return {};
8
- const cookies = {};
9
- cookieHeader.split(';').forEach(cookie => {
10
- const [name, value] = cookie.trim().split('=');
11
- if (name && value) {
12
- cookies[name] = decodeURIComponent(value);
13
- }
14
- });
15
- return cookies;
16
- }
1
+ import crypto from 'crypto';
2
+ import { getIp } from "../utils/ip.js";
3
+ import { checkKeywords } from "../security/checker.js";
4
+ import { filterXSS } from "../security/xss-filter.js";
5
+ import { createRateLimitMiddleware } from "../security/rate-limit.js";
6
+ import { store } from "../storage/store.js";
17
7
 
18
8
  /**
19
9
  * Web应用防火墙(WAF)中间件
20
- * 提供访问限流、关键词过滤、XSS防护等功能
10
+ * 改进点:
11
+ * 1. 封禁状态存服务端(Map+TTL 或 Redis),按 IP+UA 维度
12
+ * - 彻底废弃 Cookie/localStorage 标记方案(旧版可被任意绕过)
13
+ * 2. 命中恶意关键词后封禁一段时间(默认 30 分钟,可配置)
14
+ * 3. 支持配置阈值:连续命中 N 次后封禁
15
+ * 4. 安全响应头补充 X-Frame-Options / HSTS 等
16
+ * 5. 通过 store 适配层:Redis 启用时走 Redis,否则走内存,运行时异常自动降级
21
17
  */
22
18
 
23
- const WAF_BLOCKED_KEY = `${process.env.APP_NAME || 'app'}_waf_blocked`;
24
- const WAF_BLOCKED_COOKIE = 'waf_blocked';
25
- const WAF_BLOCKED_HEADER = 'x-waf-blocked';
19
+ // 封禁状态 Redis Key 前缀
20
+ const BLOCK_KEY_PREFIX = 'waf:block:';
21
+ // 封禁命中次数 Redis Key 前缀
22
+ const STRIKE_KEY_PREFIX = 'waf:strike:';
26
23
 
27
24
  /**
28
25
  * 路径白名单 - 这些路径不会被 WAF 检查
29
- * @type {Array<string>}
30
26
  */
31
27
  const WAF_PATH_WHITELIST = [
32
28
  "/.well-known/appspecific/", // Chrome DevTools
@@ -37,60 +33,119 @@ const WAF_PATH_WHITELIST = [
37
33
  "/sitemap.xml", // 站点地图
38
34
  ];
39
35
 
36
+ /**
37
+ * 可信 IP 白名单 - 这些 IP 跳过限流和封禁
38
+ * 本机回环 + RFC 1918 三大私有网段,避免开发和内网环境被误限
39
+ */
40
+ const TRUSTED_IPS = new Set(['127.0.0.1', '::1', '0.0.0.0']);
41
+
42
+ /**
43
+ * 检查 IP 是否可信(本机回环或 RFC 1918 内网)
44
+ * RFC 1918 私有网段:
45
+ * 10.0.0.0/8 (10.x.x.x)
46
+ * 172.16.0.0/12 (172.16.x.x ~ 172.31.x.x)
47
+ * 192.168.0.0/16 (192.168.x.x)
48
+ */
49
+ function isTrustedIp(ip) {
50
+ if (!ip) return false;
51
+ if (TRUSTED_IPS.has(ip)) return true;
52
+ // 10.x.x.x
53
+ if (ip.startsWith('10.')) return true;
54
+ // 192.168.x.x
55
+ if (ip.startsWith('192.168.')) return true;
56
+ // 172.16.x.x ~ 172.31.x.x
57
+ if (ip.startsWith('172.')) {
58
+ const second = parseInt(ip.split('.')[1], 10);
59
+ if (second >= 16 && second <= 31) return true;
60
+ }
61
+ return false;
62
+ }
63
+
40
64
  /**
41
65
  * 检查路径是否在白名单中
42
- * @param {string} path - 请求路径
43
- * @returns {boolean} 是否在白名单中
44
66
  */
45
67
  function isWhitelistedPath(path) {
46
- return WAF_PATH_WHITELIST.some(whitelistPath =>
68
+ return WAF_PATH_WHITELIST.some(whitelistPath =>
47
69
  path.startsWith(whitelistPath) || path === whitelistPath
48
70
  );
49
71
  }
50
72
 
73
+ /**
74
+ * 生成 IP+UA 维度的标识(sha256 摘要,避免 UA 中特殊字符影响 Key)
75
+ * @param {string} ip - 客户端 IP
76
+ * @param {string} ua - User-Agent
77
+ * @returns {string} 32 位十六进制摘要
78
+ */
79
+ function buildFingerprint(ip, ua) {
80
+ return crypto
81
+ .createHash('sha256')
82
+ .update(`${ip}|${ua || ''}`)
83
+ .digest('hex')
84
+ .slice(0, 32);
85
+ }
86
+
51
87
  /**
52
88
  * 创建 WAF 中间件
53
89
  * @private
54
90
  * @param {Object} wafConfig - WAF 配置对象
55
- * @param {boolean} wafConfig.enabled - 是否启用 WAF
56
- * @param {Object} wafConfig.rateLimit - 限流配置
57
- * @returns {Function} Express 中间件函数
58
91
  */
59
92
  function createWafMiddleware(wafConfig) {
60
93
  const rateLimitMiddleware = createRateLimitMiddleware(wafConfig.rateLimit);
61
94
 
95
+ // 封禁配置(默认 30 分钟,命中 3 次恶意关键词后封禁)
96
+ const blockConfig = wafConfig.block || {};
97
+ const BLOCK_DURATION = blockConfig.duration || 30 * 60 * 1000; // 30 分钟
98
+ const STRIKE_THRESHOLD = blockConfig.strikeThreshold || 3; // 命中 3 次封禁
99
+ const STRIKE_WINDOW = blockConfig.strikeWindow || 60 * 60 * 1000; // 1 小时内累计
100
+
62
101
  return async (req, res, next) => {
63
102
  try {
64
103
  if (!wafConfig.enabled) {
65
- console.log('[WAF] WAF已禁用,跳过检查');
66
104
  return next();
67
105
  }
68
106
 
69
107
  const clientIp = getIp(req);
108
+
109
+ // 可信 IP(本机/内网)仅跳过限流和封禁状态检查(硬约束:避免误限内网)
110
+ // 但仍执行关键词检查和 XSS 过滤,防止 X-Forwarded-For 伪造内网 IP 绕过 WAF(S-09 修复点)
111
+ const isTrusted = isTrustedIp(clientIp);
112
+
70
113
  const requestPath = req.path || "";
71
- //
114
+ const userAgent = req.headers['user-agent'] || '';
115
+ const fingerprint = buildFingerprint(clientIp, userAgent);
116
+
117
+ // 安全响应头
72
118
  res.setHeader("X-Content-Type-Options", "nosniff");
73
119
  res.setHeader("Content-Security-Policy", "frame-ancestors 'self'");
120
+ res.setHeader("X-Frame-Options", "DENY");
74
121
 
75
- if (!isWhitelistedPath(requestPath)) {
76
- const isBlockedHeader = req.headers[WAF_BLOCKED_HEADER];
77
- const cookies = parseCookies(req.headers.cookie);
78
- const isBlockedCookie = cookies[WAF_BLOCKED_COOKIE];
79
-
80
- if (isBlockedHeader === 'true' || isBlockedCookie === 'true') {
81
- console.error(`[WAF 永久封禁] IP:${clientIp} 路径:${requestPath} 原因:标记封禁`);
122
+ // 可信 IP 跳过封禁状态检查(避免误封内网);非可信 IP 才查封禁状态
123
+ if (!isTrusted && !isWhitelistedPath(requestPath)) {
124
+ const blockKey = `${BLOCK_KEY_PREFIX}${fingerprint}`;
125
+ const blocked = await store.get(blockKey);
126
+
127
+ if (blocked) {
128
+ const remaining = Math.ceil((blocked.expireAt - Date.now()) / 60000);
129
+ console.error(`[WAF 封禁拦截] IP:${clientIp} UA:${userAgent.slice(0, 50)} 剩余:${remaining}分钟`);
82
130
  return res.status(403).json({
83
131
  code: 403,
84
132
  success: false,
85
- msg: '您的访问已被限制,如需恢复请联系管理员'
133
+ msg: '您的访问已被限制,请稍后再试',
134
+ retryAfter: Math.ceil(BLOCK_DURATION / 1000),
86
135
  });
87
136
  }
88
137
  }
89
138
 
90
- await new Promise((resolve) => {
91
- rateLimitMiddleware(req, res, resolve);
92
- });
139
+ // 可信 IP 跳过限流(避免误限内网);非可信 IP 才执行限流
140
+ if (!isTrusted) {
141
+ await new Promise((resolve) => {
142
+ rateLimitMiddleware(req, res, resolve);
143
+ });
144
+ // 如果限流中间件已经响应(429),不再继续
145
+ if (res.headersSent) return;
146
+ }
93
147
 
148
+ // 收集待检查文本:路径 + query + body
94
149
  let checkText = requestPath;
95
150
  if (req.query && Object.keys(req.query).length > 0) {
96
151
  const queryStr = Object.entries(req.query)
@@ -105,6 +160,7 @@ function createWafMiddleware(wafConfig) {
105
160
 
106
161
  if (!isMultipart && req.body) {
107
162
  try {
163
+ // 先判断 body 长度再决定是否序列化,避免不必要的 CPU 消耗
108
164
  const bodyStr = typeof req.body === "string" ? req.body : JSON.stringify(req.body);
109
165
  if (bodyStr.length < 10000) {
110
166
  bodyText = ` ${bodyStr}`;
@@ -116,6 +172,7 @@ function createWafMiddleware(wafConfig) {
116
172
 
117
173
  const fullText = checkText + bodyText;
118
174
 
175
+ // 关键词检查(仅对非白名单路径)
119
176
  if (!isWhitelistedPath(requestPath)) {
120
177
  const matched = checkKeywords(fullText);
121
178
 
@@ -123,42 +180,42 @@ function createWafMiddleware(wafConfig) {
123
180
  const { category, keyword } = matched;
124
181
  console.error(`[WAF 拦截] IP:${clientIp} 路径:${requestPath} 关键词:${keyword} 类别:${category}`);
125
182
 
126
- const htmlResponse = `
127
- <!DOCTYPE html>
128
- <html>
129
- <head>
130
- <meta charset="UTF-8">
131
- <title>访问受限</title>
132
- <script>
133
- (function() {
134
- var expires = new Date();
135
- expires.setTime(expires.getTime() + ${365 * 24 * 60 * 60 * 1000});
136
- document.cookie = '${WAF_BLOCKED_COOKIE}=true; path=/; expires=' + expires.toUTCString() + '; SameSite=Lax';
137
- localStorage.setItem('${WAF_BLOCKED_KEY}', 'true');
138
- console.log('[WAF] Cookie已设置:', document.cookie);
139
- })();
140
- </script>
141
- </head>
142
- <body>
143
- <h1>非法请求</h1>
144
- <p>检测到恶意内容,您的访问已被限制。</p>
145
- <p>如需恢复请联系管理员。</p>
146
- </body>
147
- </html>
148
- `;
149
-
150
-
151
- return res.status(403).setHeader('Content-Type', 'text/html').send(htmlResponse);
183
+ // 记录命中次数,达到阈值则封禁
184
+ const strikeKey = `${STRIKE_KEY_PREFIX}${fingerprint}`;
185
+ const strikes = await store.incrAndExpire(strikeKey, STRIKE_WINDOW);
186
+
187
+ if (strikes >= STRIKE_THRESHOLD) {
188
+ // 封禁:写入封禁状态,TTL = BLOCK_DURATION
189
+ const blockKey = `${BLOCK_KEY_PREFIX}${fingerprint}`;
190
+ await store.set(blockKey, { ip: clientIp, reason: keyword, expireAt: Date.now() + BLOCK_DURATION }, BLOCK_DURATION);
191
+ console.error(`[WAF 封禁触发] IP:${clientIp} 命中次数:${strikes} 封禁时长:${BLOCK_DURATION / 60000}分钟`);
192
+
193
+ return res.status(403).json({
194
+ code: 403,
195
+ success: false,
196
+ msg: '检测到恶意访问,您的访问已被限制',
197
+ retryAfter: Math.ceil(BLOCK_DURATION / 1000),
198
+ });
199
+ }
200
+
201
+ // 未达阈值,仅拦截本次请求
202
+ return res.status(403).json({
203
+ code: 403,
204
+ success: false,
205
+ msg: '检测到非法内容,请求已被拦截',
206
+ data: { strikes, threshold: STRIKE_THRESHOLD },
207
+ });
152
208
  }
153
209
  }
154
210
 
211
+ // XSS 过滤
155
212
  try {
156
213
  if (req.query && typeof req.query === 'object') {
157
214
  const filteredQuery = filterXSS(req.query);
158
215
  Object.keys(req.query).forEach(key => delete req.query[key]);
159
216
  Object.assign(req.query, filteredQuery);
160
217
  }
161
-
218
+
162
219
  if (req.body && typeof req.body === 'object') {
163
220
  req.body = filterXSS(req.body);
164
221
  }
@@ -169,7 +226,12 @@ function createWafMiddleware(wafConfig) {
169
226
  next();
170
227
  } catch (error) {
171
228
  console.error(`[WAF 异常] IP:${getIp(req)} 错误:${error.message}`, error.stack);
172
- next();
229
+ // 异常时拒绝请求,避免 WAF 故障时被绕过(fail-closed)
230
+ return res.status(403).json({
231
+ code: 403,
232
+ success: false,
233
+ msg: '安全检查异常,请求被拒绝',
234
+ });
173
235
  }
174
236
  };
175
237
  }
@@ -180,17 +242,15 @@ function createWafMiddleware(wafConfig) {
180
242
  * @param {Object} config - WAF 配置对象
181
243
  * @param {boolean} config.enabled - 是否启用 WAF
182
244
  * @param {Object} config.rateLimit - 限流配置
183
- * @description
184
- * Express 应用配置 WAF 防护中间件
185
- * 包含访问限流、关键词过滤、XSS防护和安全响应头设置
186
- * 路径白名单中的请求不会被 WAF 检查
245
+ * @param {Object} [config.block] - 封禁配置
246
+ * @param {number} [config.block.duration=1800000] - 封禁时长(毫秒),默认 30 分钟
247
+ * @param {number} [config.block.strikeThreshold=3] - 命中恶意关键词几次后封禁
248
+ * @param {number} [config.block.strikeWindow=3600000] - 命中累计窗口(毫秒)
187
249
  * @example
188
250
  * waf(app, {
189
251
  * enabled: true,
190
- * rateLimit: {
191
- * windowMs: 60000,
192
- * max: 100
193
- * }
252
+ * rateLimit: { windowMs: 60000, max: 100 },
253
+ * block: { duration: 1800000, strikeThreshold: 3, strikeWindow: 3600000 }
194
254
  * });
195
255
  */
196
256
  export const waf = function (app, config) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "chanjs",
4
- "version": "2.7.2",
4
+ "version": "2.7.3",
5
5
  "description": "chanjs基于express5 纯js研发的轻量级mvc框架。",
6
6
  "main": "index.js",
7
7
  "module": "index.js",
@@ -20,20 +20,19 @@
20
20
  "license": "ISC",
21
21
  "dependencies": {
22
22
  "art-template": "^4.13.4",
23
- "body-parser": "^2.2.2",
24
23
  "cookie-parser": "^1.4.7",
25
24
  "cors": "^2.8.6",
26
25
  "dayjs": "^1.11.20",
27
26
  "dotenv": "^17.4.2",
28
27
  "express": "^5.2.1",
29
28
  "express-art-template": "^1.0.1",
30
- "ip2region": "^2.3.0",
31
29
  "jsonwebtoken": "^9.0.3",
32
30
  "knex": "^3.2.10",
33
31
  "marked": "^18.0.3",
34
32
  "morgan": "^1.10.1",
35
33
  "mysql2": "^3.22.3",
36
34
  "serve-favicon": "^2.5.1",
35
+ "ws": "^8.18.0",
37
36
  "xss": "^1.0.15"
38
37
  }
39
38
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * 实时通信模块 - SSE + WebSocket(零第三方依赖)
3
+ * - sse: Server-Sent Events,单向推送
4
+ * - websocket: 基于 Node.js v22.4.0+ 原生 WebSocket API
5
+ */
6
+ export { sse, SSEManager } from "./sse.js";
7
+ export { websocket, WebSocketManager } from "./websocket.js";