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.
@@ -188,26 +188,21 @@ class Repository extends BaseComponent {
188
188
  return row;
189
189
  }
190
190
 
191
- /**
192
- * 查询全部。上限口径统一收口到 config.LIMIT_MAX(this.limit):
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 size = Math.min(Math.max(Number(limit) || this.limit, 1), this.limit);
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
- /** 分页偏移查询(limit 同 all() 口径,钳制到 config.LIMIT_MAX) */
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(Math.min(Math.max(limit, 1), this.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. 静态资源
@@ -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: !!_cors.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
- // XSS 防护由下方 filterXSS 负责(输出编码),SQL 注入由参数化查询负责。
157
-
158
- // body XSS过滤
159
- // 例外:base/config 系统配置写入接口跳过 XSS 过滤——sys_config 配置值须原样存储
160
- // (含「名称 <邮箱>」发件人格式、HTML 邮件模板等),且该接口仅管理员可访问,
161
- // 配置值由后端逻辑消费而非直接反射到页面,无 XSS 风险。
162
- try {
163
- if (typeof req.body === "object" && !path.startsWith("/base/config")) req.body = filterXSS(req.body);
164
- } catch (e) {
165
- logger.error(`[WAF警告] body过滤失败 IP:${clientIp} Err:${e.message}`);
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "chanjs",
4
- "version": "2.8.6",
4
+ "version": "2.8.8",
5
5
  "description": "chanjs基于 Node.js + Express 5 的标准 HMVC 框架(NHMVC),纯 JavaScript(ESM)开发。",
6
6
  "main": "index.js",
7
7
  "module": "index.js",
@@ -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 xss(data);
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
+ };
@@ -27,8 +27,8 @@ export const arrToObj = (arr, keyField = "config_key", valueField = "config_valu
27
27
  };
28
28
 
29
29
  /**
30
- * 根据拼音/ID匹配分类,返回分类完整数据与分类ID
31
- * @param {string | number} py - 检索条件:拼音字符串 / 数字ID
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.pinyin === py || item.id === py)) || {};
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向上追溯完整父级路径(通用版,剥离硬编码pinyin业务逻辑)
80
+ * 根据节点ID向上追溯完整父级路径(通用版,不耦合具体业务字段)
81
81
  * @param {number|string} targetId 目标节点ID
82
82
  * @param {Array<Object>} list 扁平源数组
83
83
  * @param {Object} [options={}] 配置