chanjs 2.8.7 → 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/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.7",
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
+ };