chanjs 2.8.6 → 2.8.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.
- package/core/Repository.js +5 -10
- package/core/bootstrap/middleware.js +0 -14
- package/middleware/cors.js +5 -1
- package/middleware/waf.js +15 -11
- package/package.json +1 -1
- package/security/xss-filter.js +28 -2
- package/utils/data-parse.js +3 -3
- package/utils/tree.js +1 -1
package/core/Repository.js
CHANGED
|
@@ -188,26 +188,21 @@ class Repository extends BaseComponent {
|
|
|
188
188
|
return row;
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
/**
|
|
192
|
-
|
|
193
|
-
* 未传 limit 时默认取 this.limit;显式传入的 limit 也会被钳制到 [1, this.limit],
|
|
194
|
-
* 避免"all 默认 1000 / 分页上限 300"两套口径并存。
|
|
195
|
-
*/
|
|
196
|
-
async all({ query={}, sort={}, fields=[], limit }={}) {
|
|
191
|
+
/** 查询全部,默认上限1000条 */
|
|
192
|
+
async all({ query={}, sort={}, fields=[], limit=1000 }={}) {
|
|
197
193
|
const err = this._guard({ query });
|
|
198
194
|
if (err) return err;
|
|
199
|
-
const
|
|
200
|
-
const list = await this._buildBaseQuery({query,sort,fields}).limit(size);
|
|
195
|
+
const list = await this._buildBaseQuery({query,sort,fields}).limit(limit);
|
|
201
196
|
return { success:true, code:CODE_OK, msg:"查询成功", data: list };
|
|
202
197
|
}
|
|
203
198
|
|
|
204
|
-
/**
|
|
199
|
+
/** 分页偏移查询 */
|
|
205
200
|
async find({ query={}, sort={}, fields=[], limit, offset }={}) {
|
|
206
201
|
const err = this._guard({ query });
|
|
207
202
|
if (err) return err;
|
|
208
203
|
let q = this._buildBaseQuery({query,sort,fields});
|
|
209
204
|
typeof offset === 'number' && (q = q.offset(offset));
|
|
210
|
-
typeof limit === 'number' && (q = q.limit(
|
|
205
|
+
typeof limit === 'number' && (q = q.limit(limit));
|
|
211
206
|
return { success:true, code:CODE_OK, msg:"查询成功", data: await q };
|
|
212
207
|
}
|
|
213
208
|
|
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
} from "../../middleware/index.js";
|
|
5
5
|
import { health } from "../../middleware/health.js";
|
|
6
6
|
import { BODY_LIMIT } from "../../config/index.js";
|
|
7
|
-
import logger from "../../utils/logger.js";
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
9
|
* 注册核心中间件(顺序固化:不可随意调整)
|
|
@@ -18,19 +17,6 @@ export async function registerCoreMiddleware(chan) {
|
|
|
18
17
|
await waf(app, cfg.waf ?? { enabled: false });
|
|
19
18
|
// 1.5 健康检查端点(默认开启,config.health = {enabled,path} 可调;浅模式零 IO)
|
|
20
19
|
app.use(health(chan, cfg.health ?? {}));
|
|
21
|
-
// 1.6 宿主全局中间件插槽:config.middleware 数组,按声明顺序注册。
|
|
22
|
-
// 元素支持两种形态:express 中间件本身 (req,res,next);或工厂 () => express 中间件(需闭包 chan/config 时用)。
|
|
23
|
-
// 注册位置在核心链早期、模块路由之前,保证对所有响应(含静态资源)生效。
|
|
24
|
-
// 注意:app/router.js(loadCommonRouter)的执行时机在模块路由之后,app.use 在那里
|
|
25
|
-
// 只罩得住 404 兜底请求,安全响应头等全局策略应经本插槽注册而非写在 router.js。
|
|
26
|
-
for (const mw of cfg.middleware ?? []) {
|
|
27
|
-
const fn = typeof mw === "function" && mw.length <= 1 ? mw(chan) : mw;
|
|
28
|
-
if (typeof fn !== "function") {
|
|
29
|
-
logger.warn(`[Chan] config.middleware 含非法项(须为中间件或中间件工厂),已跳过: ${String(mw)}`);
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
app.use(fn);
|
|
33
|
-
}
|
|
34
20
|
// 2. Favicon
|
|
35
21
|
favicon(app);
|
|
36
22
|
// 3. 静态资源
|
package/middleware/cors.js
CHANGED
|
@@ -11,7 +11,11 @@ export const Cors = (app, _cors = {}) => {
|
|
|
11
11
|
origin: _cors.origin ?? false,
|
|
12
12
|
methods: _cors.methods ?? ["GET", "POST"],
|
|
13
13
|
allowedHeaders: _cors.allowedHeaders ?? ["Content-Type", "Authorization"],
|
|
14
|
-
credentials
|
|
14
|
+
// credentials 用 getter 包裹:每次请求实时求值(支持 _cors.credentials 本身为 getter,
|
|
15
|
+
// 例如随 originList 在 "*" 与具体域名白名单间动态切换)。
|
|
16
|
+
// 背景:W3C 规定 ACAO:* 与 ACAC:true 不能同时出现(CSRF 风险),故当 originList 为 "*"
|
|
17
|
+
// (全放行)时必须 credentials=false;具体域名白名单时 credentials=true(跨域携带 Cookie)。
|
|
18
|
+
get credentials() { return !!_cors.credentials; },
|
|
15
19
|
maxAge: _cors.maxAge ?? 600,
|
|
16
20
|
};
|
|
17
21
|
app.use(cors(safeOptions));
|
package/middleware/waf.js
CHANGED
|
@@ -15,7 +15,7 @@ const WAF_PATH_WHITELIST = [
|
|
|
15
15
|
];
|
|
16
16
|
|
|
17
17
|
// waf 配置合法键集合:启动期校验,未知键打 warn,防配置漂移静默失效
|
|
18
|
-
const WAF_KNOWN_KEYS = new Set(["enabled", "rateLimit", "ignorePaths"]);
|
|
18
|
+
const WAF_KNOWN_KEYS = new Set(["enabled", "rateLimit", "ignorePaths", "bodyXssPaths"]);
|
|
19
19
|
|
|
20
20
|
const TRUSTED_IPS = new Set(["127.0.0.1", "::1"]);
|
|
21
21
|
|
|
@@ -136,6 +136,8 @@ const createWafMiddleware = wafConfig => {
|
|
|
136
136
|
*/
|
|
137
137
|
const createWafBodyMiddleware = wafConfig => {
|
|
138
138
|
const ignorePaths = Array.isArray(wafConfig.ignorePaths) ? wafConfig.ignorePaths : [];
|
|
139
|
+
// body XSS 白名单:仅这些路径前缀的请求体做 XSS 过滤;默认空 = 全部不过滤
|
|
140
|
+
const bodyXssPaths = Array.isArray(wafConfig.bodyXssPaths) ? wafConfig.bodyXssPaths : [];
|
|
139
141
|
return async (req, res, next) => {
|
|
140
142
|
try {
|
|
141
143
|
if (!wafConfig.enabled) return next();
|
|
@@ -153,16 +155,18 @@ const createWafBodyMiddleware = wafConfig => {
|
|
|
153
155
|
// 项目 DB 全部走 knex 参数化查询(天然防 SQL 注入),后端也无任何
|
|
154
156
|
// 把用户输入交给 shell 执行的路径;而关键词扫描对留言/评论/注册等正常正文
|
|
155
157
|
// 误报极高(drop / select / alert / onclick 等普通词都会被拦),代价远大于收益。
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
// body XSS
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
158
|
+
// SQL 注入由参数化查询负责。
|
|
159
|
+
|
|
160
|
+
// body XSS 过滤(按需开启):默认不过滤任何接口的请求体。
|
|
161
|
+
// 全局默认过滤会静默改写入库内容(剥离 style、转义 iframe/script 等),
|
|
162
|
+
// 导致后台富文本编辑器的排版/特效无法保存。需要防护的接口
|
|
163
|
+
// (如会员注册、留言、评论等公开 UGC 写入)由项目在 bodyXssPaths 里声明。
|
|
164
|
+
if (bodyXssPaths.length && typeof req.body === "object" && isIgnored(path, bodyXssPaths)) {
|
|
165
|
+
try {
|
|
166
|
+
req.body = filterXSS(req.body);
|
|
167
|
+
} catch (e) {
|
|
168
|
+
logger.error(`[WAF警告] body过滤失败 IP:${clientIp} Err:${e.message}`);
|
|
169
|
+
}
|
|
166
170
|
}
|
|
167
171
|
|
|
168
172
|
next();
|
package/package.json
CHANGED
package/security/xss-filter.js
CHANGED
|
@@ -1,14 +1,40 @@
|
|
|
1
1
|
import xss from "xss";
|
|
2
2
|
|
|
3
|
+
// 以 xss 库默认白名单为底(覆盖 a/p/div/section/video/audio/table 等常规标签),
|
|
4
|
+
// 所有标签放行 style / class / id —— 保留排版与样式,过滤不再「太严格」;
|
|
5
|
+
// 另补 iframe(src 仅 https),用于合法的视频外链嵌入。
|
|
6
|
+
const whiteList = xss.getDefaultWhiteList();
|
|
7
|
+
for (const tag of Object.keys(whiteList)) {
|
|
8
|
+
whiteList[tag] = [...new Set([...(whiteList[tag] ?? []), "style", "class", "id"])];
|
|
9
|
+
}
|
|
10
|
+
whiteList.iframe = ["src", "width", "height", "frameborder", "allow", "allowfullscreen", "style", "class", "id"];
|
|
11
|
+
|
|
12
|
+
// 自定义过滤:只拦真正危险的内容
|
|
13
|
+
const filter = new xss.FilterXSS({
|
|
14
|
+
whiteList,
|
|
15
|
+
onTagAttr: (tag, name, value) => {
|
|
16
|
+
// on* 事件属性(onclick/onerror/onload 等)
|
|
17
|
+
if (name.startsWith("on")) return "";
|
|
18
|
+
// javascript: / vbscript: 协议
|
|
19
|
+
if ((name === "href" || name === "src") && /^\s*(javascript|vbscript):/i.test(value)) return "";
|
|
20
|
+
// iframe src 仅允许 https 外链
|
|
21
|
+
if (tag === "iframe" && name === "src" && !/^https:\/\//i.test(value)) return "";
|
|
22
|
+
},
|
|
23
|
+
// 白名单外标签(如 <script>)转义为纯文本保留,内容不丢、不可执行
|
|
24
|
+
stripIgnoreTag: false,
|
|
25
|
+
});
|
|
26
|
+
|
|
3
27
|
/**
|
|
4
28
|
* 递归深度过滤对象/数组/字符串XSS,WeakSet防循环引用栈溢出
|
|
29
|
+
* 策略:宽白名单 —— 保留 style/class/id 与常规标签,仅剥离脚本执行能力
|
|
30
|
+
*(on* 事件、javascript:/vbscript: 协议、非 https 的 iframe、<script> 实体)。
|
|
5
31
|
* @param {any} data 待清洗数据
|
|
6
32
|
* @param {WeakSet} [visited] 循环引用标记缓存
|
|
7
33
|
* @returns {any} 清洗后数据
|
|
8
34
|
*/
|
|
9
35
|
export const filterXSS = (data, visited) => {
|
|
10
36
|
// 字符串直接过滤
|
|
11
|
-
if (typeof data === "string") return
|
|
37
|
+
if (typeof data === "string") return filter.process(data);
|
|
12
38
|
|
|
13
39
|
// 数组遍历递归
|
|
14
40
|
if (Array.isArray(data)) return data.map(item => filterXSS(item, visited));
|
|
@@ -28,4 +54,4 @@ export const filterXSS = (data, visited) => {
|
|
|
28
54
|
|
|
29
55
|
// number / boolean / null / undefined 直接原值返回
|
|
30
56
|
return data;
|
|
31
|
-
};
|
|
57
|
+
};
|
package/utils/data-parse.js
CHANGED
|
@@ -27,8 +27,8 @@ export const arrToObj = (arr, keyField = "config_key", valueField = "config_valu
|
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
31
|
-
* @param {string | number} py -
|
|
30
|
+
* 根据 slug / ID 匹配分类,返回分类完整数据与分类ID
|
|
31
|
+
* @param {string | number} py - 检索条件:slug 字符串 / 数字ID
|
|
32
32
|
* @param {Array<Object>} source - 分类数据源数组
|
|
33
33
|
* @returns {{ cate: Object, id: string|number }}
|
|
34
34
|
*/
|
|
@@ -37,6 +37,6 @@ export function getChildrenId(py, source) {
|
|
|
37
37
|
logger.warn("[getChildrenId] 数据源source不是数组", source);
|
|
38
38
|
return { cate: {}, id: "" };
|
|
39
39
|
}
|
|
40
|
-
const cate = source.find(item => item && typeof item === "object" && (item.
|
|
40
|
+
const cate = source.find(item => item && typeof item === "object" && (item.slug === py || item.id === py)) || {};
|
|
41
41
|
return { cate, id: cate.id ?? "" };
|
|
42
42
|
}
|
package/utils/tree.js
CHANGED
|
@@ -77,7 +77,7 @@ export function tree(list, rootPid = 0, options = {}) {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
|
-
* 根据节点ID
|
|
80
|
+
* 根据节点ID向上追溯完整父级路径(通用版,不耦合具体业务字段)
|
|
81
81
|
* @param {number|string} targetId 目标节点ID
|
|
82
82
|
* @param {Array<Object>} list 扁平源数组
|
|
83
83
|
* @param {Object} [options={}] 配置
|