chanjs 2.7.6 → 2.7.7
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/core/Repository.js +11 -2
- package/middleware/static.js +1 -1
- package/middleware/validate.js +4 -0
- package/middleware/waf.js +2 -2
- package/package.json +1 -1
- package/response/code.js +1 -1
- package/security/checker.js +21 -2
package/core/Repository.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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/middleware/static.js
CHANGED
|
@@ -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,
|
|
17
|
+
app.use(prefix, express.static(dir, { maxAge: maxAge ?? 0, dotfiles: "deny" }));
|
|
18
18
|
});
|
|
19
19
|
};
|
package/middleware/validate.js
CHANGED
|
@@ -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
package/response/code.js
CHANGED
package/security/checker.js
CHANGED
|
@@ -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
|
-
|
|
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..]是每个关键词的捕获组
|