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 +15 -11
- package/package.json +1 -1
- package/security/xss-filter.js +28 -2
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
|
+
};
|