chanjs 2.7.4 → 2.7.5
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/USAGE.md +533 -0
- package/config/index.js +37 -6
- package/core/App.js +166 -0
- package/core/Container.js +77 -0
- package/core/Controller.js +29 -0
- package/core/Database.js +93 -0
- package/core/Repository.js +327 -0
- package/core/Service.js +11 -0
- package/core/bootstrap/error-handler.js +104 -0
- package/core/bootstrap/hook-runner.js +64 -0
- package/core/bootstrap/middleware.js +35 -0
- package/core/bootstrap/router-loader.js +53 -0
- package/core/errors.js +224 -0
- package/core/loader.js +89 -0
- package/core/registry.js +17 -0
- package/doc/Cache.md +279 -106
- package/doc/Common.md +590 -134
- package/doc/Controller.md +166 -95
- package/doc/Help.md +299 -698
- package/doc/QuickStart.md +116 -0
- package/doc/Repository.md +560 -0
- package/doc/Service.md +201 -527
- package/index.js +75 -37
- package/middleware/body.js +17 -0
- package/middleware/cookie.js +7 -15
- package/middleware/cors.js +9 -27
- package/middleware/favicon.js +7 -17
- package/middleware/header.js +15 -16
- package/middleware/index.js +11 -11
- package/middleware/log.js +26 -56
- package/middleware/static.js +15 -28
- package/middleware/template.js +75 -115
- package/middleware/validate.js +79 -0
- package/middleware/waf.js +174 -197
- package/package.json +9 -2
- package/response/code.js +73 -0
- package/response/index.js +9 -6
- package/response/response.js +82 -236
- package/security/checker.js +26 -74
- package/security/index.js +4 -9
- package/security/jwt.js +69 -142
- package/security/keywords.js +32 -136
- package/security/rate-limit.js +38 -80
- package/security/sign.js +83 -176
- package/security/xss-filter.js +21 -53
- package/storage/cache.js +57 -196
- package/storage/index.js +3 -6
- package/storage/redis.js +123 -181
- package/storage/store.js +163 -188
- package/utils/data-parse.js +42 -186
- package/utils/file.js +73 -244
- package/utils/filter.js +22 -25
- package/utils/html.js +49 -33
- package/utils/index.js +21 -7
- package/utils/ip.js +31 -71
- package/utils/logger.js +117 -0
- package/utils/pages.js +55 -0
- package/utils/paths.js +18 -0
- package/utils/request.js +94 -136
- package/utils/signal.js +87 -0
- package/utils/time.js +33 -75
- package/utils/tree.js +112 -104
- package/App.js +0 -533
- package/base/Aop.js +0 -195
- package/base/Container.js +0 -161
- package/base/Controller.js +0 -65
- package/base/Database.js +0 -133
- package/base/Event.js +0 -61
- package/base/Repository.js +0 -644
- package/common/api.js +0 -35
- package/common/code.js +0 -52
- package/common/email.js +0 -191
- package/common/index.js +0 -5
- package/common/pages.js +0 -120
- package/common/utils.js +0 -73
- package/config/code.js +0 -166
- package/config/paths.js +0 -60
- package/doc/Aop.md +0 -269
- package/doc/Email.md +0 -114
- package/doc/Event.md +0 -232
- package/global/env.js +0 -11
- package/global/import.js +0 -39
- package/global/index.js +0 -8
- package/helper/index.js +0 -79
- package/loader/index.js +0 -6
- package/loader/loader.js +0 -138
- package/middleware/compress.js +0 -185
- package/middleware/setBody.js +0 -32
- package/realtime/index.js +0 -7
- package/realtime/sse.js +0 -424
- package/realtime/websocket.js +0 -540
- package/schedule/index.js +0 -6
- package/schedule/schedule.js +0 -491
package/middleware/waf.js
CHANGED
|
@@ -1,259 +1,236 @@
|
|
|
1
|
-
import crypto from
|
|
1
|
+
import crypto from "crypto";
|
|
2
2
|
import { getIp } from "../utils/ip.js";
|
|
3
3
|
import { checkKeywords } from "../security/checker.js";
|
|
4
4
|
import { filterXSS } from "../security/xss-filter.js";
|
|
5
5
|
import { createRateLimitMiddleware } from "../security/rate-limit.js";
|
|
6
6
|
import { store } from "../storage/store.js";
|
|
7
|
+
import { CODE_BLOCKED } from "../response/code.js";
|
|
8
|
+
import logger from "../utils/logger.js";
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* 改进点:
|
|
11
|
-
* 1. 封禁状态存服务端(Map+TTL 或 Redis),按 IP+UA 维度
|
|
12
|
-
* - 彻底废弃 Cookie/localStorage 标记方案(旧版可被任意绕过)
|
|
13
|
-
* 2. 命中恶意关键词后封禁一段时间(默认 30 分钟,可配置)
|
|
14
|
-
* 3. 支持配置阈值:连续命中 N 次后封禁
|
|
15
|
-
* 4. 安全响应头补充 X-Frame-Options / HSTS 等
|
|
16
|
-
* 5. 通过 store 适配层:Redis 启用时走 Redis,否则走内存,运行时异常自动降级
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
// 封禁状态 Redis Key 前缀
|
|
20
|
-
const BLOCK_KEY_PREFIX = 'waf:block:';
|
|
21
|
-
// 封禁命中次数 Redis Key 前缀
|
|
22
|
-
const STRIKE_KEY_PREFIX = 'waf:strike:';
|
|
10
|
+
const BLOCK_KEY_PREFIX = "waf:block:";
|
|
11
|
+
const STRIKE_KEY_PREFIX = "waf:strike:";
|
|
23
12
|
|
|
24
|
-
|
|
25
|
-
* 路径白名单 - 这些路径不会被 WAF 检查
|
|
26
|
-
*/
|
|
13
|
+
// 放行路径白名单
|
|
27
14
|
const WAF_PATH_WHITELIST = [
|
|
28
|
-
"/.well-known/appspecific/",
|
|
29
|
-
"/.well-known/change-password",
|
|
30
|
-
"/favicon.ico",
|
|
31
|
-
"/robots.txt",
|
|
32
|
-
"/llms.txt",
|
|
33
|
-
"/sitemap.xml",
|
|
15
|
+
"/.well-known/appspecific/",
|
|
16
|
+
"/.well-known/change-password",
|
|
17
|
+
"/favicon.ico",
|
|
18
|
+
"/robots.txt",
|
|
19
|
+
"/llms.txt",
|
|
20
|
+
"/sitemap.xml",
|
|
34
21
|
];
|
|
35
22
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
23
|
+
const TRUSTED_IPS = new Set(["127.0.0.1", "::1"]);
|
|
24
|
+
|
|
25
|
+
const DEFAULT_BLOCK = {
|
|
26
|
+
BLOCK_DURATION: 30 * 60 * 1000,
|
|
27
|
+
STRIKE_THRESHOLD: 3,
|
|
28
|
+
STRIKE_WINDOW: 60 * 60 * 1000,
|
|
29
|
+
};
|
|
41
30
|
|
|
42
31
|
/**
|
|
43
|
-
*
|
|
44
|
-
* RFC 1918 私有网段:
|
|
45
|
-
* 10.0.0.0/8 (10.x.x.x)
|
|
46
|
-
* 172.16.0.0/12 (172.16.x.x ~ 172.31.x.x)
|
|
47
|
-
* 192.168.0.0/16 (192.168.x.x)
|
|
32
|
+
* 判断内网/回环可信IP
|
|
48
33
|
*/
|
|
49
|
-
|
|
34
|
+
const isTrustedIp = ip => {
|
|
50
35
|
if (!ip) return false;
|
|
51
36
|
if (TRUSTED_IPS.has(ip)) return true;
|
|
52
|
-
|
|
53
|
-
if (ip.startsWith(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// 172.16.x.x ~ 172.31.x.x
|
|
57
|
-
if (ip.startsWith('172.')) {
|
|
58
|
-
const second = parseInt(ip.split('.')[1], 10);
|
|
59
|
-
if (second >= 16 && second <= 31) return true;
|
|
37
|
+
if (ip.startsWith("10.") || ip.startsWith("192.168.")) return true;
|
|
38
|
+
if (ip.startsWith("172.")) {
|
|
39
|
+
const seg = Number(ip.split(".")[1]);
|
|
40
|
+
return seg >= 16 && seg <= 31;
|
|
60
41
|
}
|
|
61
42
|
return false;
|
|
62
|
-
}
|
|
43
|
+
};
|
|
63
44
|
|
|
64
45
|
/**
|
|
65
|
-
*
|
|
46
|
+
* 路径白名单匹配
|
|
66
47
|
*/
|
|
67
|
-
|
|
68
|
-
return WAF_PATH_WHITELIST.some(whitelistPath =>
|
|
69
|
-
path.startsWith(whitelistPath) || path === whitelistPath
|
|
70
|
-
);
|
|
71
|
-
}
|
|
48
|
+
const isWhitelistedPath = path => WAF_PATH_WHITELIST.some(p => path === p || path.startsWith(p));
|
|
72
49
|
|
|
73
50
|
/**
|
|
74
|
-
*
|
|
75
|
-
* @param {string} ip - 客户端 IP
|
|
76
|
-
* @param {string} ua - User-Agent
|
|
77
|
-
* @returns {string} 32 位十六进制摘要
|
|
51
|
+
* IP+UA指纹 sha256(前32位)
|
|
78
52
|
*/
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
.createHash('sha256')
|
|
82
|
-
.update(`${ip}|${ua || ''}`)
|
|
83
|
-
.digest('hex')
|
|
84
|
-
.slice(0, 32);
|
|
85
|
-
}
|
|
53
|
+
const buildFingerprint = (ip, ua = "") =>
|
|
54
|
+
crypto.createHash("sha256").update(`${ip}|${ua}`).digest("hex").slice(0, 32);
|
|
86
55
|
|
|
87
56
|
/**
|
|
88
|
-
*
|
|
89
|
-
* @private
|
|
90
|
-
* @param {Object} wafConfig - WAF 配置对象
|
|
57
|
+
* 覆盖Express5只读req.query
|
|
91
58
|
*/
|
|
92
|
-
|
|
93
|
-
|
|
59
|
+
const overrideQuery = (req, cleanQuery) => {
|
|
60
|
+
Object.defineProperty(req, "query", {
|
|
61
|
+
value: cleanQuery,
|
|
62
|
+
writable: true,
|
|
63
|
+
enumerable: true,
|
|
64
|
+
configurable: true,
|
|
65
|
+
});
|
|
66
|
+
};
|
|
94
67
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
68
|
+
/**
|
|
69
|
+
* 统一 WAF 拦截响应
|
|
70
|
+
*/
|
|
71
|
+
const respondWaf = (res, status, msg, data) =>
|
|
72
|
+
res.status(status).json({ code: CODE_BLOCKED, success: false, msg, data });
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 关键词命中计数+封禁逻辑
|
|
76
|
+
*/
|
|
77
|
+
async function handleStrike(fingerprint, clientIp, keyword, blockCfg) {
|
|
78
|
+
const strikeKey = `${STRIKE_KEY_PREFIX}${fingerprint}`;
|
|
79
|
+
const strikes = await store.incrAndExpire(strikeKey, blockCfg.STRIKE_WINDOW);
|
|
80
|
+
|
|
81
|
+
if (strikes >= blockCfg.STRIKE_THRESHOLD) {
|
|
82
|
+
const blockKey = `${BLOCK_KEY_PREFIX}${fingerprint}`;
|
|
83
|
+
await store.set(blockKey, { ip: clientIp, reason: keyword, expireAt: Date.now() + blockCfg.BLOCK_DURATION }, blockCfg.BLOCK_DURATION);
|
|
84
|
+
logger.error(`[WAF封禁] IP:${clientIp} 累计命中:${strikes}次 封禁${blockCfg.BLOCK_DURATION / 60000}分钟`);
|
|
85
|
+
return { blocked: true, strikes };
|
|
86
|
+
}
|
|
87
|
+
return { blocked: false, strikes };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 执行限流,返回是否已响应 */
|
|
91
|
+
const runRateLimit = (rateLimit, req, res) =>
|
|
92
|
+
new Promise(rateLimit.bind(null, req, res, r)).then(() => res.headersSent);
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 前置WAF中间件(body前)
|
|
96
|
+
*/
|
|
97
|
+
const createWafMiddleware = wafConfig => {
|
|
98
|
+
const rateLimit = createRateLimitMiddleware(wafConfig.rateLimit);
|
|
99
|
+
const blockCfg = { ...DEFAULT_BLOCK, ...wafConfig.block };
|
|
100
100
|
|
|
101
101
|
return async (req, res, next) => {
|
|
102
102
|
try {
|
|
103
|
-
if (!wafConfig.enabled)
|
|
103
|
+
if (!wafConfig.enabled) return next();
|
|
104
|
+
|
|
105
|
+
const clientIp = getIp(req);
|
|
106
|
+
const path = req.path || "";
|
|
107
|
+
const ua = req.headers["user-agent"] || "";
|
|
108
|
+
const fp = buildFingerprint(clientIp, ua);
|
|
109
|
+
const whitePath = isWhitelistedPath(path);
|
|
110
|
+
|
|
111
|
+
// 可信IP直接放行,仅做query XSS过滤
|
|
112
|
+
if (isTrustedIp(clientIp)) {
|
|
113
|
+
if (req.query && Object.keys(req.query).length) overrideQuery(req, filterXSS(req.query));
|
|
104
114
|
return next();
|
|
105
115
|
}
|
|
106
116
|
|
|
107
|
-
|
|
117
|
+
// 限流
|
|
118
|
+
if (await runRateLimit(rateLimit, req, res)) return;
|
|
108
119
|
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const userAgent = req.headers['user-agent'] || '';
|
|
115
|
-
const fingerprint = buildFingerprint(clientIp, userAgent);
|
|
116
|
-
|
|
117
|
-
// 安全响应头
|
|
118
|
-
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
119
|
-
res.setHeader("Content-Security-Policy", "frame-ancestors 'self'");
|
|
120
|
-
res.setHeader("X-Frame-Options", "DENY");
|
|
121
|
-
|
|
122
|
-
// 可信 IP 跳过封禁状态检查(避免误封内网);非可信 IP 才查封禁状态
|
|
123
|
-
if (!isTrusted && !isWhitelistedPath(requestPath)) {
|
|
124
|
-
const blockKey = `${BLOCK_KEY_PREFIX}${fingerprint}`;
|
|
125
|
-
const blocked = await store.get(blockKey);
|
|
126
|
-
|
|
127
|
-
if (blocked) {
|
|
128
|
-
const remaining = Math.ceil((blocked.expireAt - Date.now()) / 60000);
|
|
129
|
-
console.error(`[WAF 封禁拦截] IP:${clientIp} UA:${userAgent.slice(0, 50)} 剩余:${remaining}分钟`);
|
|
130
|
-
return res.status(403).json({
|
|
131
|
-
code: 403,
|
|
132
|
-
success: false,
|
|
133
|
-
msg: '您的访问已被限制,请稍后再试',
|
|
134
|
-
retryAfter: Math.ceil(BLOCK_DURATION / 1000),
|
|
135
|
-
});
|
|
136
|
-
}
|
|
120
|
+
// 已封禁直接拦截
|
|
121
|
+
const blockInfo = await store.get(`${BLOCK_KEY_PREFIX}${fp}`);
|
|
122
|
+
if (blockInfo) {
|
|
123
|
+
logger.error(`[WAF拦截封禁] IP:${clientIp} UA:${ua.slice(0, 50)}`);
|
|
124
|
+
return respondWaf(res, 403, "检测到恶意访问,您的访问已被限制", { retryAfter: Math.ceil(blockCfg.BLOCK_DURATION / 1000) });
|
|
137
125
|
}
|
|
138
126
|
|
|
139
|
-
//
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
});
|
|
144
|
-
// 如果限流中间件已经响应(429),不再继续
|
|
145
|
-
if (res.headersSent) return;
|
|
127
|
+
// 白名单路径跳过关键词检测
|
|
128
|
+
if (whitePath) {
|
|
129
|
+
if (req.query && Object.keys(req.query).length) overrideQuery(req, filterXSS(req.query));
|
|
130
|
+
return next();
|
|
146
131
|
}
|
|
147
132
|
|
|
148
|
-
//
|
|
149
|
-
let checkText =
|
|
150
|
-
if (
|
|
151
|
-
const queryStr = Object.entries(req.query)
|
|
152
|
-
.map(([k, v]) => `${k}=${v}`)
|
|
153
|
-
.join(" ");
|
|
133
|
+
// 路径+query拼接检测恶意关键词
|
|
134
|
+
let checkText = path;
|
|
135
|
+
if (Object.keys(req.query ?? {}).length) {
|
|
136
|
+
const queryStr = Object.entries(req.query).map(([k, v]) => `${k}=${v}`).join(" ");
|
|
154
137
|
checkText += ` ${queryStr}`;
|
|
155
138
|
}
|
|
139
|
+
const hit = checkKeywords(checkText);
|
|
140
|
+
if (hit) {
|
|
141
|
+
const { keyword, category } = hit;
|
|
142
|
+
logger.error(`[WAF拦截-URL] IP:${clientIp} Path:${path} Key:${keyword} Type:${category}`);
|
|
143
|
+
const { blocked, strikes } = await handleStrike(fp, clientIp, keyword, blockCfg);
|
|
144
|
+
if (blocked) return respondWaf(res, 403, "检测到恶意访问,您的访问已被限制", { retryAfter: Math.ceil(blockCfg.BLOCK_DURATION / 1000) });
|
|
145
|
+
return respondWaf(res, 403, "检测到非法内容,请求已被拦截", { strikes, threshold: blockCfg.STRIKE_THRESHOLD });
|
|
146
|
+
}
|
|
156
147
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
try {
|
|
163
|
-
// 先判断 body 长度再决定是否序列化,避免不必要的 CPU 消耗
|
|
164
|
-
const bodyStr = typeof req.body === "string" ? req.body : JSON.stringify(req.body);
|
|
165
|
-
if (bodyStr.length < 10000) {
|
|
166
|
-
bodyText = ` ${bodyStr}`;
|
|
167
|
-
}
|
|
168
|
-
} catch (e) {
|
|
169
|
-
console.error(`[WAF 警告] body序列化失败 | IP:${clientIp} 错误:${e.message}`);
|
|
170
|
-
}
|
|
148
|
+
// query XSS净化
|
|
149
|
+
try {
|
|
150
|
+
if (req.query && Object.keys(req.query).length) overrideQuery(req, filterXSS(req.query));
|
|
151
|
+
} catch (e) {
|
|
152
|
+
logger.error(`[WAF警告] query过滤失败 IP:${clientIp} Err:${e.message}`);
|
|
171
153
|
}
|
|
172
154
|
|
|
173
|
-
|
|
155
|
+
next();
|
|
156
|
+
} catch (err) {
|
|
157
|
+
logger.error(`[WAF前置异常] IP:${getIp(req)} Msg:${err.message}`, err.stack);
|
|
158
|
+
return respondWaf(res, 403, "安全检查异常,请求被拒绝");
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
};
|
|
174
162
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
163
|
+
/**
|
|
164
|
+
* 已登录用户的管理接口路径前缀(跳过 body 关键词检测,避免误杀富文本/代码内容)
|
|
165
|
+
*/
|
|
166
|
+
const BODY_CHECK_SKIP_PREFIXES = ["/cms/", "/base/", "/member/", "/book/", "/oss/", "/vip/"];
|
|
178
167
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
168
|
+
/**
|
|
169
|
+
* Body层WAF中间件(body解析后)
|
|
170
|
+
*/
|
|
171
|
+
const createWafBodyMiddleware = wafConfig => {
|
|
172
|
+
const blockCfg = { ...DEFAULT_BLOCK, ...wafConfig.block };
|
|
182
173
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
174
|
+
return async (req, res, next) => {
|
|
175
|
+
try {
|
|
176
|
+
if (!wafConfig.enabled) return next();
|
|
177
|
+
const path = req.path || "";
|
|
178
|
+
if (isWhitelistedPath(path)) return next();
|
|
186
179
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
180
|
+
const clientIp = getIp(req);
|
|
181
|
+
const contentType = req.headers["content-type"]?.toLowerCase() || "";
|
|
182
|
+
// 上传文件跳过body文本检测
|
|
183
|
+
if (contentType.includes("multipart/form-data")) return next();
|
|
184
|
+
// 空body直接放行
|
|
185
|
+
if (!req.body || (typeof req.body === "object" && !Object.keys(req.body).length)) return next();
|
|
192
186
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
success: false,
|
|
196
|
-
msg: '检测到恶意访问,您的访问已被限制',
|
|
197
|
-
retryAfter: Math.ceil(BLOCK_DURATION / 1000),
|
|
198
|
-
});
|
|
199
|
-
}
|
|
187
|
+
// 已登录用户的管理接口:跳过关键词检测(富文本/代码内容会误杀),仅做 XSS 过滤
|
|
188
|
+
const isAuthedAdmin = req.user?.uid && BODY_CHECK_SKIP_PREFIXES.some(p => path.startsWith(p));
|
|
200
189
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
});
|
|
190
|
+
if (!isAuthedAdmin) {
|
|
191
|
+
// 序列化body文本,截断1w字符防绕过
|
|
192
|
+
let bodyText = "";
|
|
193
|
+
try {
|
|
194
|
+
bodyText = (typeof req.body === "string" ? req.body : JSON.stringify(req.body)).slice(0, 10000);
|
|
195
|
+
} catch (e) {
|
|
196
|
+
logger.error(`[WAF警告] body序列化失败 IP:${clientIp} Err:${e.message}`);
|
|
208
197
|
}
|
|
209
|
-
}
|
|
210
198
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
199
|
+
// body关键词检测拦截
|
|
200
|
+
if (bodyText) {
|
|
201
|
+
const hit = checkKeywords(bodyText);
|
|
202
|
+
if (hit) {
|
|
203
|
+
const { keyword, category } = hit;
|
|
204
|
+
const fp = buildFingerprint(clientIp, req.headers["user-agent"] || "");
|
|
205
|
+
logger.error(`[WAF拦截-Body] IP:${clientIp} Path:${path} Key:${keyword} Type:${category}`);
|
|
206
|
+
const { blocked, strikes } = await handleStrike(fp, clientIp, keyword, blockCfg);
|
|
207
|
+
if (blocked) return respondWaf(res, 403, "检测到恶意访问,您的访问已被限制", { retryAfter: Math.ceil(blockCfg.BLOCK_DURATION / 1000) });
|
|
208
|
+
return respondWaf(res, 403, "检测到非法内容,请求已被拦截", { strikes, threshold: blockCfg.STRIKE_THRESHOLD });
|
|
209
|
+
}
|
|
217
210
|
}
|
|
211
|
+
}
|
|
218
212
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
213
|
+
// body XSS过滤
|
|
214
|
+
try {
|
|
215
|
+
if (typeof req.body === "object") req.body = filterXSS(req.body);
|
|
222
216
|
} catch (e) {
|
|
223
|
-
|
|
217
|
+
logger.error(`[WAF警告] body过滤失败 IP:${clientIp} Err:${e.message}`);
|
|
224
218
|
}
|
|
225
219
|
|
|
226
220
|
next();
|
|
227
|
-
} catch (
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
return res.status(403).json({
|
|
231
|
-
code: 403,
|
|
232
|
-
success: false,
|
|
233
|
-
msg: '安全检查异常,请求被拒绝',
|
|
234
|
-
});
|
|
221
|
+
} catch (err) {
|
|
222
|
+
logger.error(`[WAF Body异常] IP:${getIp(req)} Msg:${err.message}`);
|
|
223
|
+
return respondWaf(res, 403, "安全检查异常,请求被拒绝");
|
|
235
224
|
}
|
|
236
225
|
};
|
|
237
|
-
}
|
|
226
|
+
};
|
|
238
227
|
|
|
239
228
|
/**
|
|
240
|
-
*
|
|
241
|
-
* @param {Object} app - Express 应用实例
|
|
242
|
-
* @param {Object} config - WAF 配置对象
|
|
243
|
-
* @param {boolean} config.enabled - 是否启用 WAF
|
|
244
|
-
* @param {Object} config.rateLimit - 限流配置
|
|
245
|
-
* @param {Object} [config.block] - 封禁配置
|
|
246
|
-
* @param {number} [config.block.duration=1800000] - 封禁时长(毫秒),默认 30 分钟
|
|
247
|
-
* @param {number} [config.block.strikeThreshold=3] - 命中恶意关键词几次后封禁
|
|
248
|
-
* @param {number} [config.block.strikeWindow=3600000] - 命中累计窗口(毫秒)
|
|
249
|
-
* @example
|
|
250
|
-
* waf(app, {
|
|
251
|
-
* enabled: true,
|
|
252
|
-
* rateLimit: { windowMs: 60000, max: 100 },
|
|
253
|
-
* block: { duration: 1800000, strikeThreshold: 3, strikeWindow: 3600000 }
|
|
254
|
-
* });
|
|
229
|
+
* 注册WAF前置中间件(body前)
|
|
255
230
|
*/
|
|
256
|
-
export const waf =
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
231
|
+
export const waf = (app, config) => app.use(createWafMiddleware(config));
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 注册WAF Body中间件(body后)
|
|
235
|
+
*/
|
|
236
|
+
export const wafBody = (app, config) => app.use(createWafBodyMiddleware(config));
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "chanjs",
|
|
4
|
-
"version": "2.7.
|
|
4
|
+
"version": "2.7.5",
|
|
5
5
|
"description": "chanjs基于express5 纯js研发的轻量级mvc框架。",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"module": "index.js",
|
|
@@ -32,8 +32,15 @@
|
|
|
32
32
|
"morgan": "^1.10.1",
|
|
33
33
|
"mysql2": "^3.22.3",
|
|
34
34
|
"serve-favicon": "^2.5.1",
|
|
35
|
-
"ws": "^8.18.0",
|
|
36
35
|
"xss": "^1.0.15",
|
|
37
36
|
"ioredis": "^5.4.6"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"zod": "^3.0.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependenciesMeta": {
|
|
42
|
+
"zod": {
|
|
43
|
+
"optional": true
|
|
44
|
+
}
|
|
38
45
|
}
|
|
39
46
|
}
|
package/response/code.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 业务状态码统一规范:与HTTP状态码解耦,0为成功,分段划分错误域
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// 业务码-默认提示文案映射
|
|
6
|
+
export const CODE = Object.freeze({
|
|
7
|
+
// 成功
|
|
8
|
+
0: "操作成功",
|
|
9
|
+
// 1xxx 通用业务错误
|
|
10
|
+
1001: "认证失败",
|
|
11
|
+
1002: "令牌已过期",
|
|
12
|
+
1003: "权限不足",
|
|
13
|
+
1004: "资源不存在",
|
|
14
|
+
1005: "资源已存在",
|
|
15
|
+
1006: "参数无效",
|
|
16
|
+
1007: "参数缺失",
|
|
17
|
+
1008: "业务处理失败",
|
|
18
|
+
1009: "请求过于频繁",
|
|
19
|
+
1010: "登录设备异常",
|
|
20
|
+
1011: "访问已被限制",
|
|
21
|
+
// 5xxx 系统错误
|
|
22
|
+
5001: "系统内部错误",
|
|
23
|
+
5002: "服务繁忙,请稍后再试",
|
|
24
|
+
// 6xxx 数据库错误
|
|
25
|
+
6001: "数据库连接失败",
|
|
26
|
+
6002: "数据库访问被拒绝",
|
|
27
|
+
6003: "存在关联数据,操作失败",
|
|
28
|
+
6004: "数据库字段错误",
|
|
29
|
+
6005: "数据重复,违反唯一性约束",
|
|
30
|
+
6006: "目标表不存在",
|
|
31
|
+
6007: "数据库操作超时",
|
|
32
|
+
6008: "数据库语法错误,请检查查询语句",
|
|
33
|
+
6009: "数据库连接已关闭,请重试",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// 语义化常量(框架内直接引用,避免硬编码数字)
|
|
37
|
+
export const CODE_OK = 0;
|
|
38
|
+
export const CODE_AUTH_FAILED = 1001;
|
|
39
|
+
export const CODE_TOKEN_EXPIRED = 1002;
|
|
40
|
+
export const CODE_FORBIDDEN = 1003;
|
|
41
|
+
export const CODE_NOT_FOUND = 1004;
|
|
42
|
+
export const CODE_CONFLICT = 1005;
|
|
43
|
+
export const CODE_PARAM_INVALID = 1006;
|
|
44
|
+
export const CODE_PARAM_MISSING = 1007;
|
|
45
|
+
export const CODE_BUSINESS_FAIL = 1008;
|
|
46
|
+
export const CODE_RATE_LIMIT = 1009;
|
|
47
|
+
export const CODE_DEVICE_ERROR = 1010;
|
|
48
|
+
export const CODE_BLOCKED = 1011;
|
|
49
|
+
export const CODE_SYSTEM_ERROR = 5001;
|
|
50
|
+
export const CODE_SERVICE_BUSY = 5002;
|
|
51
|
+
export const CODE_DB_CONNECTION_ERROR = 6001;
|
|
52
|
+
export const CODE_DB_ACCESS_DENIED = 6002;
|
|
53
|
+
export const CODE_DB_OPERATION_TIMEOUT = 6007;
|
|
54
|
+
|
|
55
|
+
// 数据库原生错误标识 → 业务码映射
|
|
56
|
+
export const DB_ERROR = Object.freeze({
|
|
57
|
+
ECONNREFUSED: 6001,
|
|
58
|
+
ER_ACCESS_DENIED_ERROR: 6002,
|
|
59
|
+
ER_ROW_IS_REFERENCED_2: 6003,
|
|
60
|
+
ER_BAD_FIELD_ERROR: 6004,
|
|
61
|
+
ER_DUP_ENTRY: 6005,
|
|
62
|
+
ER_NO_SUCH_TABLE: 6006,
|
|
63
|
+
ETIMEOUT: 6007,
|
|
64
|
+
ER_TABLE_EXISTS_ERROR: 1005,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 根据业务码获取默认提示文案
|
|
69
|
+
* @param {number} code 业务状态码
|
|
70
|
+
* @param {string} [fallback="操作失败"] 兜底文案
|
|
71
|
+
* @returns {string}
|
|
72
|
+
*/
|
|
73
|
+
export const getCodeMsg = (code, fallback = "操作失败") => CODE[code] ?? fallback;
|
package/response/index.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* response 模块聚合导出
|
|
3
|
+
* 对外暴露:success / fail / routeNotFound / serializeError
|
|
4
4
|
*/
|
|
5
5
|
export {
|
|
6
6
|
success,
|
|
7
7
|
fail,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
routeNotFound,
|
|
9
|
+
serializeError,
|
|
10
|
+
buildErrorHtml,
|
|
11
|
+
respondError,
|
|
12
12
|
} from "./response.js";
|
|
13
|
+
|
|
14
|
+
// CODE 字典对外暴露(业务侧 common.CODE 需要)
|
|
15
|
+
export { CODE } from "./code.js";
|