chanjs 2.7.6 → 2.7.8

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.
@@ -152,7 +152,7 @@ class Repository extends BaseComponent {
152
152
  this._checkDB();
153
153
  if (!Object.keys(data).length) return { success:false, code:CODE_PARAM_MISSING, msg:"参数缺失", data:{} };
154
154
  const res = await this.db(this.tableName).insert(this.#formatDate(data));
155
- return { success:true, code:CODE_OK, msg:"插入成功", data:{ insertId:res[0], affectedRows:res.length } };
155
+ return { success:true, code:CODE_OK, msg:"插入成功", data:{ insertId:res[0], affectedRows:1 } };
156
156
  }
157
157
 
158
158
  /** 批量插入 */
@@ -161,13 +161,18 @@ class Repository extends BaseComponent {
161
161
  if (!records.length) return { success:false, code:CODE_PARAM_MISSING, msg:"参数缺失", data:{} };
162
162
  const list = records.map(r => this.#formatDate(r));
163
163
  const res = await this.db(this.tableName).insert(list);
164
- return { success:true, code:CODE_OK, msg:"批量插入成功", data:{ insertId:res[0], affectedRows:res.length } };
164
+ return { success:true, code:CODE_OK, msg:"批量插入成功", data:{ insertId:res[0], affectedRows:records.length } };
165
165
  }
166
166
 
167
167
  /** 条件删除 */
168
168
  async del(query={}) {
169
169
  this._checkDB();
170
170
  if (!Object.keys(query).length) return { success:false, code:CODE_PARAM_MISSING, msg:"参数缺失", data:{} };
171
+ // 防御纵深:若传入的查询条件字段全部非法(被 applyQuery 静默丢弃),
172
+ // 会生成不带 WHERE 的全表删除,这里 fail-closed 拒绝执行。
173
+ if (!Object.keys(query).some(f => SORT_FIELD_REGEX.test(f))) {
174
+ return { success:false, code:CODE_PARAM_INVALID, msg:"删除条件非法", data:{} };
175
+ }
171
176
  const rows = await applyQuery(this.db(this.tableName), query).del();
172
177
  return { success:true, code:CODE_OK, msg:"删除成功", data:{ affectedRows:rows } };
173
178
  }
@@ -191,6 +196,10 @@ class Repository extends BaseComponent {
191
196
  if (!query || !data || !Object.keys(query).length || !Object.keys(data).length) {
192
197
  return { success:false, code:CODE_PARAM_INVALID, msg:"参数无效", data:{} };
193
198
  }
199
+ // 防御纵深:与 del 同理,条件字段全部非法时会生成无 WHERE 的全表更新,fail-closed 拒绝。
200
+ if (!Object.keys(query).some(f => SORT_FIELD_REGEX.test(f))) {
201
+ return { success:false, code:CODE_PARAM_INVALID, msg:"更新条件非法", data:{} };
202
+ }
194
203
  const rows = await applyQuery(this.db(this.tableName), query).update(this.#formatDate(data));
195
204
  return { success:true, code:CODE_OK, msg:"更新成功", data:{ affectedRows:rows } };
196
205
  }
package/index.js CHANGED
@@ -34,7 +34,7 @@ export {
34
34
  export { success, fail, routeNotFound, serializeError, buildErrorHtml, respondError } from "./response/index.js";
35
35
 
36
36
  // ===================== 安全工具 =====================
37
- export { setToken, getToken, verifyToken } from "./security/jwt.js";
37
+ export { setToken, getToken, verifyToken, revokeToken } from "./security/jwt.js";
38
38
  export { aesEncrypt, aesDecrypt } from "./security/sign.js";
39
39
  export { createRateLimitMiddleware } from "./security/rate-limit.js";
40
40
  export { filterXSS, checkKeywords } from "./security/index.js";
@@ -14,6 +14,6 @@ export const staticMw = async (app, statics) => {
14
14
  logger.error(`[Static] 不安全目录拦截:${dir}`);
15
15
  return;
16
16
  }
17
- app.use(prefix, express.static(dir, { maxAge: maxAge ?? 0, dotfile: "deny" }));
17
+ app.use(prefix, express.static(dir, { maxAge: maxAge ?? 0, dotfiles: "deny" }));
18
18
  });
19
19
  };
@@ -62,6 +62,7 @@ export function validate(schema, source = 'body') {
62
62
  * @returns {Function} Express 中间件
63
63
  */
64
64
  export function validateAll(schemas) {
65
+ const aggregated = {};
65
66
  return (req, res, next) => {
66
67
  for (const [source, schema] of Object.entries(schemas)) {
67
68
  if (!schema) continue;
@@ -73,7 +74,10 @@ export function validateAll(schemas) {
73
74
  return next(err);
74
75
  }
75
76
  setValidated(req, source, result.data);
77
+ aggregated[source] = result.data;
76
78
  }
79
+ // 聚合多来源校验结果,避免用单一来源覆盖已校验数据(修复原末行用 req.body 覆盖问题)
80
+ req.validated = aggregated;
77
81
  next();
78
82
  };
79
83
  }
package/middleware/waf.js CHANGED
@@ -198,9 +198,9 @@ const createWafBodyMiddleware = wafConfig => {
198
198
  logger.error(`[WAF警告] body序列化失败 IP:${clientIp} Err:${e.message}`);
199
199
  }
200
200
 
201
- // body关键词检测拦截
201
+ // body关键词检测拦截(scope='body' 跳过路径/文件特征类规则,降低正文误报)
202
202
  if (bodyText) {
203
- const hit = checkKeywords(bodyText);
203
+ const hit = checkKeywords(bodyText, { scope: 'body' });
204
204
  if (hit) {
205
205
  const { keyword, category } = hit;
206
206
  const fp = buildFingerprint(clientIp, req.headers["user-agent"] || "");
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "chanjs",
4
- "version": "2.7.6",
4
+ "version": "2.7.8",
5
5
  "description": "chanjs基于express5 纯js研发的轻量级mvc框架。",
6
6
  "main": "index.js",
7
7
  "module": "index.js",
@@ -36,7 +36,7 @@
36
36
  "ioredis": "^5.4.6"
37
37
  },
38
38
  "peerDependencies": {
39
- "zod": "^3.0.0"
39
+ "zod": "^4.4.3"
40
40
  },
41
41
  "peerDependenciesMeta": {
42
42
  "zod": {
package/response/code.js CHANGED
@@ -60,7 +60,7 @@ export const DB_ERROR = Object.freeze({
60
60
  ER_BAD_FIELD_ERROR: 6004,
61
61
  ER_DUP_ENTRY: 6005,
62
62
  ER_NO_SUCH_TABLE: 6006,
63
- ETIMEOUT: 6007,
63
+ ETIMEDOUT: 6007,
64
64
  ER_TABLE_EXISTS_ERROR: 1005,
65
65
  });
66
66
 
@@ -1,15 +1,34 @@
1
1
  import { KEYWORD_RULES, keywordRegexCache, SORTED_CATEGORIES } from "./keywords.js";
2
2
 
3
+ /**
4
+ * Body 扫描时应跳过的分类:这些规则主要描述 URL/路径/文件特征
5
+ * (如 secret/debug/metadata/setup 等英文词、.env/.git 后缀、wholeWord 代码特征),
6
+ * 在正文(评论/文章/会员资料)里极易误杀正常内容,而真实攻击已由
7
+ * sqlInjection / xss / commandInjection / pathTraversal 覆盖。
8
+ */
9
+ const BODY_EXCLUDED_CATEGORIES = [
10
+ "wholeWord",
11
+ "extensions",
12
+ "directories",
13
+ "sensitiveIdentifiers",
14
+ "encoding",
15
+ ];
16
+
3
17
  /**
4
18
  * 恶意关键词检测:单次遍历预排序的分类列表,利用捕获组直接定位命中关键词
5
19
  * @param {string} fullText 待检测文本
20
+ * @param {{scope?: 'body'|'url'}} [options] scope='body' 时跳过路径/文件特征类规则,降低正文误报
6
21
  * @returns {{category: string; keyword: string}|null}
7
22
  */
8
- export const checkKeywords = fullText => {
23
+ export const checkKeywords = (fullText, { scope } = {}) => {
9
24
  const text = fullText?.trim();
10
25
  if (!text) return null;
11
26
 
12
- for (const cat of SORTED_CATEGORIES) {
27
+ const cats = scope === 'body'
28
+ ? SORTED_CATEGORIES.filter(c => !BODY_EXCLUDED_CATEGORIES.includes(c))
29
+ : SORTED_CATEGORIES;
30
+
31
+ for (const cat of cats) {
13
32
  const match = keywordRegexCache[cat].merged.exec(text);
14
33
  if (!match) continue;
15
34
  // capture[0]是整体匹配,capture[1..]是每个关键词的捕获组