pure-js-db 2.53.0 → 2.53.1
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/README.md +15 -3
- package/package.json +10 -1
- package/src/ai/ai-service.js +23 -43
- package/src/alerting/alert-manager.js +33 -4
- package/src/audit/audit-log.js +53 -2
- package/src/backup/backup-manager.js +39 -6
- package/src/cluster/raft-http-transport.js +14 -4
- package/src/executor/executor.js +24 -1
- package/src/executor/wal.js +81 -11
- package/src/graphql-bridge.js +45 -1
- package/src/http-api.js +32 -2
- package/src/http-server/index.js +2 -0
- package/src/http-server/server.js +13 -0
- package/src/http-server/ws-server.js +6 -2
- package/src/replication/replication-http-transport.js +16 -4
- package/src/sdk/java/pom.xml +1 -1
- package/src/sdk/java/pureclient/PureClient.java +1 -1
- package/src/sdk/java/pureclient/PureClientTest.java +1 -1
- package/src/sdk/php/composer.json +1 -1
- package/src/sdk/php/src/PureClient.php +1 -1
- package/src/sdk/python/pure_client.py +1 -1
- package/src/sdk/rust/Cargo.toml +1 -1
- package/src/sdk/rust/RELEASE-CHECKLIST.md +2 -2
- package/src/sdk/rust/src/lib.rs +1 -1
- package/src/security/sql-injection-detector.js +288 -0
- package/src/security/ssrf-guard.js +78 -0
- package/src/sync/sync-engine.js +7 -3
- package/src/vector/distributed-vector-search.js +3 -3
- package/src/version.js +1 -1
|
@@ -85,6 +85,225 @@ function _evalTautology(numLeft, numOp, numRight, strLeft, strOp, strRight, bool
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
|
|
88
|
+
// ── 常量表达式恒真检测(2026-10-31 渗透测试 H-01 修复)──
|
|
89
|
+
// 原 TAUTOLOGY_RE 只覆盖「数字比较 / 字符串相等 / TRUE/FALSE / NULL IS」。
|
|
90
|
+
// 实测 `OR 1 BETWEEN 1 AND 1`、`OR 'a' LIKE 'a'`、`OR 1 IN (1)`、`OR (1=1)`、
|
|
91
|
+
// `OR NOT 1=2` 等恒真变体可绕过 HTTP 层 critical/high 门禁。
|
|
92
|
+
// 以下实现一个轻量 tokenizer + 递归下降求值器,专门识别「连接词后跟纯常量表达式」
|
|
93
|
+
// 的恒真/恒假构造(BETWEEN / IN / LIKE / 括号 / NOT / IS NULL / 比较)。
|
|
94
|
+
function _constTokenize(str) {
|
|
95
|
+
const tokens = [];
|
|
96
|
+
let i = 0;
|
|
97
|
+
const n = str.length;
|
|
98
|
+
while (i < n) {
|
|
99
|
+
const c = str[i];
|
|
100
|
+
if (/\s/.test(c)) { i++; continue; }
|
|
101
|
+
if (c === '(' || c === ')') { tokens.push({ type: 'paren', value: c }); i++; continue; }
|
|
102
|
+
if (c === ',') { tokens.push({ type: 'comma' }); i++; continue; }
|
|
103
|
+
// 字符串字面量('' 转义)
|
|
104
|
+
if (c === "'") {
|
|
105
|
+
let j = i + 1;
|
|
106
|
+
let val = '';
|
|
107
|
+
while (j < n) {
|
|
108
|
+
if (str[j] === "'") {
|
|
109
|
+
if (str[j + 1] === "'") { val += "'"; j += 2; continue; }
|
|
110
|
+
j++;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
val += str[j];
|
|
114
|
+
j++;
|
|
115
|
+
}
|
|
116
|
+
if (j > n) return null; // 未闭合
|
|
117
|
+
tokens.push({ type: 'string', value: val });
|
|
118
|
+
i = j;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
// 数字(含 +/- 前缀、小数、科学计数法)
|
|
122
|
+
if (/[0-9]/.test(c) || ((c === '-' || c === '+') && /[0-9]/.test(str[i + 1]))) {
|
|
123
|
+
let j = i + 1;
|
|
124
|
+
while (j < n && /[0-9.eE]/.test(str[j])) j++;
|
|
125
|
+
// 处理 1e-5 / 1E+3 形式的尾随符号
|
|
126
|
+
if (j < n && (str[j] === '-' || str[j] === '+') && (str[j - 1] === 'e' || str[j - 1] === 'E')) j++;
|
|
127
|
+
while (j < n && /[0-9]/.test(str[j])) j++;
|
|
128
|
+
const num = Number(str.slice(i, j));
|
|
129
|
+
if (Number.isNaN(num)) return null;
|
|
130
|
+
tokens.push({ type: 'number', value: num });
|
|
131
|
+
i = j;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
// 比较运算符
|
|
135
|
+
const two = str.slice(i, i + 2);
|
|
136
|
+
if (two === '<=' || two === '>=' || two === '<>' || two === '!=') {
|
|
137
|
+
tokens.push({ type: 'op', value: two });
|
|
138
|
+
i += 2;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (c === '=' || c === '<' || c === '>') {
|
|
142
|
+
tokens.push({ type: 'op', value: c });
|
|
143
|
+
i++;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
// 关键字 / 标识符
|
|
147
|
+
if (/[A-Za-z_]/.test(c)) {
|
|
148
|
+
let j = i;
|
|
149
|
+
while (j < n && /[A-Za-z0-9_]/.test(str[j])) j++;
|
|
150
|
+
const word = str.slice(i, j).toUpperCase();
|
|
151
|
+
i = j;
|
|
152
|
+
if (word === 'AND' || word === 'OR' || word === 'XOR' || word === 'NOT'
|
|
153
|
+
|| word === 'BETWEEN' || word === 'IN' || word === 'LIKE' || word === 'IS'
|
|
154
|
+
|| word === 'NULL' || word === 'TRUE' || word === 'FALSE') {
|
|
155
|
+
tokens.push({ type: 'kw', value: word });
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
return null; // 出现列名等标识符 → 非纯常量表达式,跳过
|
|
159
|
+
}
|
|
160
|
+
return null; // 未知字符(含 -- 注释、LIMIT 等尾随关键字)
|
|
161
|
+
}
|
|
162
|
+
return tokens;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function _likeMatch(value, pattern) {
|
|
166
|
+
// 将 SQL LIKE 模式(% 任意序列、_ 单字符、\ 转义)转为正则做常量求值
|
|
167
|
+
let re = '^';
|
|
168
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
169
|
+
const ch = pattern[i];
|
|
170
|
+
if (ch === '%') re += '.*';
|
|
171
|
+
else if (ch === '_') re += '.';
|
|
172
|
+
else if (ch === '\\' && i + 1 < pattern.length) {
|
|
173
|
+
re += pattern[i + 1].replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
174
|
+
i++;
|
|
175
|
+
} else {
|
|
176
|
+
re += ch.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
re += '$';
|
|
180
|
+
try {
|
|
181
|
+
return new RegExp(re).test(value);
|
|
182
|
+
} catch {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function _constParse(tokens) {
|
|
188
|
+
let pos = 0;
|
|
189
|
+
const peek = () => tokens[pos];
|
|
190
|
+
const next = () => tokens[pos++];
|
|
191
|
+
const expect = (type, val) => {
|
|
192
|
+
const t = next();
|
|
193
|
+
if (!t || t.type !== type || (val !== undefined && t.value !== val)) {
|
|
194
|
+
throw new Error('unexpected token');
|
|
195
|
+
}
|
|
196
|
+
return t;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
function parseOr() {
|
|
200
|
+
let left = parseAnd();
|
|
201
|
+
while (peek() && peek().type === 'kw' && (peek().value === 'OR' || peek().value === 'XOR')) {
|
|
202
|
+
const op = next().value;
|
|
203
|
+
const right = parseAnd();
|
|
204
|
+
left = op === 'XOR' ? Boolean(left) !== Boolean(right) : (left || right);
|
|
205
|
+
}
|
|
206
|
+
return left;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function parseAnd() {
|
|
210
|
+
let left = parseNot();
|
|
211
|
+
while (peek() && peek().type === 'kw' && peek().value === 'AND') {
|
|
212
|
+
next();
|
|
213
|
+
const right = parseNot();
|
|
214
|
+
left = left && right;
|
|
215
|
+
}
|
|
216
|
+
return left;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function parseNot() {
|
|
220
|
+
if (peek() && peek().type === 'kw' && peek().value === 'NOT') {
|
|
221
|
+
next();
|
|
222
|
+
return !parseNot();
|
|
223
|
+
}
|
|
224
|
+
// 括号表达式:OR (1=1) / AND (1=1 OR 2=2)
|
|
225
|
+
if (peek() && peek().type === 'paren' && peek().value === '(') {
|
|
226
|
+
next();
|
|
227
|
+
const v = parseOr();
|
|
228
|
+
expect('paren', ')');
|
|
229
|
+
return v;
|
|
230
|
+
}
|
|
231
|
+
return parseComparison();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function parseValue() {
|
|
235
|
+
const t = next();
|
|
236
|
+
if (!t) throw new Error('eof');
|
|
237
|
+
if (t.type === 'number' || t.type === 'string') return t.value;
|
|
238
|
+
if (t.type === 'kw') {
|
|
239
|
+
if (t.value === 'TRUE') return true;
|
|
240
|
+
if (t.value === 'FALSE') return false;
|
|
241
|
+
if (t.value === 'NULL') return null;
|
|
242
|
+
}
|
|
243
|
+
throw new Error('not a constant');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function parseComparison() {
|
|
247
|
+
const left = parseValue();
|
|
248
|
+
|
|
249
|
+
// BETWEEN low AND high
|
|
250
|
+
if (peek() && peek().type === 'kw' && peek().value === 'BETWEEN') {
|
|
251
|
+
next();
|
|
252
|
+
const low = parseValue();
|
|
253
|
+
expect('kw', 'AND');
|
|
254
|
+
const high = parseValue();
|
|
255
|
+
return left !== null && left >= low && left <= high;
|
|
256
|
+
}
|
|
257
|
+
// IN (v1, v2, ...)
|
|
258
|
+
if (peek() && peek().type === 'kw' && peek().value === 'IN') {
|
|
259
|
+
next();
|
|
260
|
+
expect('paren', '(');
|
|
261
|
+
const vals = [parseValue()];
|
|
262
|
+
while (peek() && peek().type === 'comma') {
|
|
263
|
+
next();
|
|
264
|
+
vals.push(parseValue());
|
|
265
|
+
}
|
|
266
|
+
expect('paren', ')');
|
|
267
|
+
return left !== null && vals.some(v => v === left);
|
|
268
|
+
}
|
|
269
|
+
// LIKE pattern
|
|
270
|
+
if (peek() && peek().type === 'kw' && peek().value === 'LIKE') {
|
|
271
|
+
next();
|
|
272
|
+
const pattern = parseValue();
|
|
273
|
+
if (left === null || typeof left !== 'string' || typeof pattern !== 'string') return false;
|
|
274
|
+
return _likeMatch(left, pattern);
|
|
275
|
+
}
|
|
276
|
+
// IS [NOT] NULL
|
|
277
|
+
if (peek() && peek().type === 'kw' && peek().value === 'IS') {
|
|
278
|
+
next();
|
|
279
|
+
let negate = false;
|
|
280
|
+
if (peek() && peek().type === 'kw' && peek().value === 'NOT') {
|
|
281
|
+
next();
|
|
282
|
+
negate = true;
|
|
283
|
+
}
|
|
284
|
+
expect('kw', 'NULL');
|
|
285
|
+
const isNull = left === null;
|
|
286
|
+
return negate ? !isNull : isNull;
|
|
287
|
+
}
|
|
288
|
+
// 比较
|
|
289
|
+
if (peek() && peek().type === 'op') {
|
|
290
|
+
const op = next().value;
|
|
291
|
+
const right = parseValue();
|
|
292
|
+
// eslint-disable-next-line eqeqeq -- 与 SQL 语义对齐('1' == 1 → true)
|
|
293
|
+
if (op === '=') return left == right;
|
|
294
|
+
// eslint-disable-next-line eqeqeq
|
|
295
|
+
if (op === '<>' || op === '!=') return left != right;
|
|
296
|
+
if (op === '<') return left < right;
|
|
297
|
+
if (op === '>') return left > right;
|
|
298
|
+
if (op === '<=') return left <= right;
|
|
299
|
+
if (op === '>=') return left >= right;
|
|
300
|
+
}
|
|
301
|
+
// 无比较运算符 → 裸布尔值
|
|
302
|
+
return Boolean(left);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return parseOr();
|
|
306
|
+
}
|
|
88
307
|
// 治理立项④(docs/internal/audit/2026-09-26-eslint-security-triage.md 第四节):检测器输入独立长度上限。
|
|
89
308
|
// 上游 executor 全局 SQL 上限为 1MB(2026-08-15 生产审计 M-01);本检测器逐模式扫描为
|
|
90
309
|
// CPU 密集路径,超长输入不再逐模式扫描,直接按 HIGH 级别判为可疑(fail-closed:
|
|
@@ -269,6 +488,7 @@ export class SQLInjectionDetector {
|
|
|
269
488
|
this._detectFileSystemAccess,
|
|
270
489
|
this._detectSystemCommand,
|
|
271
490
|
this._detectAlwaysTrueCondition,
|
|
491
|
+
this._detectConstTautology,
|
|
272
492
|
];
|
|
273
493
|
}
|
|
274
494
|
|
|
@@ -618,6 +838,74 @@ export class SQLInjectionDetector {
|
|
|
618
838
|
}
|
|
619
839
|
return null;
|
|
620
840
|
}
|
|
841
|
+
|
|
842
|
+
// ── 检测器 11: 常量恒真/恒假表达式(2026-10-31 渗透测试 H-01 修复)──
|
|
843
|
+
// 捕获 `OR 1 BETWEEN 1 AND 1`、`OR 'a' LIKE 'a'`、`OR 1 IN (1)`、`OR (1=1)`、
|
|
844
|
+
// `OR NOT 1=2`、`OR 1 IS NOT NULL` 等纯常量恒真变体(原 TAUTOLOGY_RE 未覆盖)。
|
|
845
|
+
_detectConstTautology(sql, _upperSql, rawSql, _rawUpper) {
|
|
846
|
+
const findings = [];
|
|
847
|
+
const push = (type, severity, confidence, evidence, description) => {
|
|
848
|
+
findings.push(new InjectionFinding({ type, severity, confidence, evidence, description }));
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
const run = (text, inStripped) => {
|
|
852
|
+
// 剥离 SQL 注释,避免 -- / # / /* */ 截断检测
|
|
853
|
+
const cleaned = text
|
|
854
|
+
.replace(/--[^\n]*/g, ' ')
|
|
855
|
+
.replace(/#[^\n]*/g, ' ')
|
|
856
|
+
.replace(/\/\*[\s\S]*?\*\//g, ' ');
|
|
857
|
+
const connectorRe = /(?:^|[\s('])(OR|AND|XOR)\s+/gi;
|
|
858
|
+
const matches = cleaned.matchAll(connectorRe);
|
|
859
|
+
for (const m of matches) {
|
|
860
|
+
const op = m[1].toUpperCase();
|
|
861
|
+
// BETWEEN 子句内部的 AND 不是逻辑连接词,跳过以避免误报(如 `b BETWEEN 1 AND 2`)
|
|
862
|
+
if (op === 'AND' && /BETWEEN\s+[^;()]*\s*$/i.test(cleaned.slice(0, m.index))) {
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
const rest = cleaned.slice(m.index + m[0].length);
|
|
866
|
+
let truth;
|
|
867
|
+
try {
|
|
868
|
+
const tokens = _constTokenize(rest);
|
|
869
|
+
if (!tokens || tokens.length === 0) continue;
|
|
870
|
+
truth = _constParse(tokens);
|
|
871
|
+
} catch {
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
if (typeof truth !== 'boolean') continue;
|
|
875
|
+
const evidence = (m[0].trim() + ' ' + rest.trim()).slice(0, 120);
|
|
876
|
+
if ((op === 'OR' || op === 'XOR') && truth === true) {
|
|
877
|
+
push(
|
|
878
|
+
'boolean-blind',
|
|
879
|
+
inStripped ? SEVERITY.CRITICAL : SEVERITY.HIGH,
|
|
880
|
+
inStripped ? 0.9 : 0.8,
|
|
881
|
+
evidence,
|
|
882
|
+
`Boolean-based blind injection: ${op} 常量恒真表达式(${evidence})`,
|
|
883
|
+
);
|
|
884
|
+
} else if (op === 'AND' && truth === true) {
|
|
885
|
+
push(
|
|
886
|
+
'boolean-blind',
|
|
887
|
+
SEVERITY.HIGH,
|
|
888
|
+
0.8,
|
|
889
|
+
evidence,
|
|
890
|
+
`Boolean-based blind injection: AND 常量恒真表达式(${evidence})`,
|
|
891
|
+
);
|
|
892
|
+
} else if (op === 'AND' && truth === false) {
|
|
893
|
+
push(
|
|
894
|
+
'boolean-blind',
|
|
895
|
+
SEVERITY.MEDIUM,
|
|
896
|
+
0.6,
|
|
897
|
+
evidence,
|
|
898
|
+
`Boolean-based blind injection: AND 常量恒假探测(${evidence})`,
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
run(sql, true);
|
|
905
|
+
run(rawSql, false);
|
|
906
|
+
|
|
907
|
+
return findings.length > 0 ? findings : null;
|
|
908
|
+
}
|
|
621
909
|
}
|
|
622
910
|
|
|
623
911
|
// ── 全局实例(便捷使用)──
|
|
@@ -319,6 +319,8 @@ export class SSRFGuard {
|
|
|
319
319
|
this.maxRedirects = opts.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
|
|
320
320
|
this.requestTimeoutMs = opts.requestTimeoutMs ?? 10000;
|
|
321
321
|
this.followRedirects = opts.followRedirects !== false;
|
|
322
|
+
// 2026-10-31 M-04:validateDeep 的 DNS 解析超时(毫秒)
|
|
323
|
+
this.dnsTimeoutMs = opts.dnsTimeoutMs ?? 3000;
|
|
322
324
|
|
|
323
325
|
// 统计计数器
|
|
324
326
|
this._stats = { allowed: 0, blocked: 0, redirected: 0 };
|
|
@@ -584,6 +586,82 @@ export class SSRFGuard {
|
|
|
584
586
|
|
|
585
587
|
// ── 安全 fetch(含 DNS 校验 + 重定向跟踪)──
|
|
586
588
|
|
|
589
|
+
/**
|
|
590
|
+
* 深度 URL 校验 = 静态校验 + DNS 解析校验(2026-10-31 渗透测试 M-04 修复)
|
|
591
|
+
*
|
|
592
|
+
* 生产出站 HTTP 此前只用静态 validate()(不解析 DNS、不处理重定向),
|
|
593
|
+
* 攻击者控制域名解析到内网/云元数据(169.254.169.254)即可绕过。
|
|
594
|
+
* validateDeep 在静态校验基础上异步解析 DNS 并复验解析 IP,
|
|
595
|
+
* 调用方应在实际发出请求前 await 本方法。
|
|
596
|
+
*
|
|
597
|
+
* @param {string} urlStr - 待校验 URL
|
|
598
|
+
* @returns {Promise<{allowed: boolean, reason?: string, host?: string, ips?: string[]}>}
|
|
599
|
+
*/
|
|
600
|
+
async validateDeep(urlStr) {
|
|
601
|
+
const check = this.validate(urlStr);
|
|
602
|
+
if (!check.allowed) {
|
|
603
|
+
return check;
|
|
604
|
+
}
|
|
605
|
+
let dns;
|
|
606
|
+
try {
|
|
607
|
+
// 加 3s 超时,避免 DNS 解析缓慢拖住生产出站请求
|
|
608
|
+
dns = await Promise.race([
|
|
609
|
+
this.validateDNS(check.host),
|
|
610
|
+
new Promise(resolve => setTimeout(() => resolve({ allowed: true, timeout: true }), this.dnsTimeoutMs || 3000)),
|
|
611
|
+
]);
|
|
612
|
+
} catch {
|
|
613
|
+
dns = { allowed: true, resolutionFailed: true };
|
|
614
|
+
}
|
|
615
|
+
if (!dns.allowed) {
|
|
616
|
+
// DNS 解析失败(ENOTFOUND/EAI_AGAIN 等):实际请求同样无法解析目标主机,
|
|
617
|
+
// 无 SSRF 风险,放行交由下层连接报错(同时兼容测试中不可解析的 mock 主机名)。
|
|
618
|
+
if (dns.reason && dns.reason.includes('DNS 解析失败')) {
|
|
619
|
+
return { allowed: true, host: check.host, resolutionFailed: true };
|
|
620
|
+
}
|
|
621
|
+
return {
|
|
622
|
+
allowed: false,
|
|
623
|
+
reason: dns.reason,
|
|
624
|
+
host: check.host,
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
return { allowed: true, host: check.host, ips: dns.ips };
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* 解析并返回可直连的钉扎目标(2026-10-31 渗透测试 R6 修复)
|
|
632
|
+
*
|
|
633
|
+
* 供基于 node:http/node:https 的传输层(非 fetch 路径)使用:
|
|
634
|
+
* 返回已通过校验的解析 IP 与原始 Host 头(含端口),调用方应把
|
|
635
|
+
* `hostname` 设为 `ip`,并在请求头保留 `Host: hostHeader`(https 还需
|
|
636
|
+
* `servername: 原始主机名` 以维持 SNI)。这消除「DNS 校验与实际请求之间
|
|
637
|
+
* DNS 再次变化」的 rebinding TOCTOU 窗口。
|
|
638
|
+
*
|
|
639
|
+
* @param {string} urlStr - 目标 URL
|
|
640
|
+
* @returns {Promise<{ip: string, hostHeader: string, hostname: string, port: number}>}
|
|
641
|
+
* @throws {Error} 校验失败/无可用 IP 时抛错(code='SSRF_BLOCKED')
|
|
642
|
+
*/
|
|
643
|
+
async resolvePinnedHost(urlStr) {
|
|
644
|
+
const check = this.validate(urlStr);
|
|
645
|
+
if (!check.allowed) {
|
|
646
|
+
const err = new Error(`SSRF blocked: ${check.reason}`);
|
|
647
|
+
err.code = 'SSRF_BLOCKED';
|
|
648
|
+
throw err;
|
|
649
|
+
}
|
|
650
|
+
const dns = await this.validateDNS(check.host);
|
|
651
|
+
if (!dns.allowed || !dns.ips || dns.ips.length === 0) {
|
|
652
|
+
const err = new Error(`SSRF pinned DNS blocked: ${dns.reason || '无可解析 IP'}`);
|
|
653
|
+
err.code = 'SSRF_BLOCKED';
|
|
654
|
+
throw err;
|
|
655
|
+
}
|
|
656
|
+
const url = new URL(urlStr);
|
|
657
|
+
return {
|
|
658
|
+
ip: dns.ips[0],
|
|
659
|
+
hostHeader: url.host,
|
|
660
|
+
hostname: url.hostname,
|
|
661
|
+
port: url.port ? Number.parseInt(url.port, 10) : (url.protocol === 'https:' ? 443 : 80),
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
587
665
|
/**
|
|
588
666
|
* 包裹 fetch 的安全请求方法
|
|
589
667
|
*
|
package/src/sync/sync-engine.js
CHANGED
|
@@ -356,11 +356,11 @@ export class SyncEngine {
|
|
|
356
356
|
* @returns {Promise<Object|null>}
|
|
357
357
|
*/
|
|
358
358
|
async _httpRequest(method, path, body = null) {
|
|
359
|
-
// v1.34.5: SSRF 防护校验服务端 URL(
|
|
359
|
+
// v1.34.5: SSRF 防护校验服务端 URL(2026-10-31 M-04:升级为静态+DNS 深度校验)
|
|
360
360
|
const fullUrl = `${this.serverUrl}${path}`;
|
|
361
361
|
const guard = await this._getSSRFGuard();
|
|
362
362
|
if (guard) {
|
|
363
|
-
const check = guard.
|
|
363
|
+
const check = await guard.validateDeep(fullUrl);
|
|
364
364
|
if (!check.allowed) {
|
|
365
365
|
if (this._onError) {
|
|
366
366
|
try {
|
|
@@ -381,7 +381,11 @@ export class SyncEngine {
|
|
|
381
381
|
options.body = JSON.stringify(body);
|
|
382
382
|
}
|
|
383
383
|
|
|
384
|
-
|
|
384
|
+
// 2026-10-31 M-04:浏览器路径若已配置 SSRF guard,改用 safeFetch 以在
|
|
385
|
+
// 重定向逐跳时重新校验(防止 302 跳到内网/云元数据)。
|
|
386
|
+
const response = guard
|
|
387
|
+
? await guard.safeFetch(fullUrl, options)
|
|
388
|
+
: await fetch(fullUrl, options);
|
|
385
389
|
if (!response.ok) {
|
|
386
390
|
return null;
|
|
387
391
|
}
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* - 接口: POST /search { vector, k, options } → { results: [{rowId, distance}] }
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
|
|
33
33
|
// ═══════════════════════════════════════════
|
|
34
34
|
// 分片策略
|
|
@@ -260,11 +260,11 @@ class RemoteVectorShard {
|
|
|
260
260
|
throw new Error(`远程分片 ${this.shardId} 无可用 fetch 函数`);
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
-
// v1.34.5: SSRF 防护校验远程分片 endpoint
|
|
263
|
+
// v1.34.5: SSRF 防护校验远程分片 endpoint(2026-10-31 M-04:升级为静态+DNS 深度校验)
|
|
264
264
|
const requestUrl = `${this.endpoint}/search`;
|
|
265
265
|
const guard = await this._getSSRFGuard();
|
|
266
266
|
if (guard) {
|
|
267
|
-
const check = guard.
|
|
267
|
+
const check = await guard.validateDeep(requestUrl);
|
|
268
268
|
if (!check.allowed) {
|
|
269
269
|
throw new Error(`SSRF 阻止远程分片 ${this.shardId} 请求: ${check.reason}`);
|
|
270
270
|
}
|
package/src/version.js
CHANGED
|
@@ -14,6 +14,6 @@
|
|
|
14
14
|
* 并校验所有 JS 文件不再硬编码版本号。
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
export const VERSION = '2.53.
|
|
17
|
+
export const VERSION = '2.53.1';
|
|
18
18
|
|
|
19
19
|
export const PHASE = 'v2.53.0 性能服务化(P0-5/P0-6 请求级查询取消与超时):执行器新增 options 级取消检查 _abortCheck(直接读 options.signal,避免共享 Executor 并发异步查询互相覆盖实例级取消状态),接线到全部分片让出检查点(分片过滤/分片 GROUP BY/_finishSelectAfterRows 阶段边界/JOIN 派发前/parallelHashJoinWithWorker 键计算让出点);executeSQLAsync 与 parallelHashJoinWithWorker 对取消错误(QUERY_ABORTED)上抛而非静默回退同步;routeSQL(sql, params, res) 为每个请求创建 AbortController——客户端断开(res 提前 close)即中止 + this._timeout 超时中止,取消在让出检查点真实生效(P0-6);query handler 主查询与 SELECT 端点传递 res;新增 query-cancel.test.js 6 项(预中止/JOIN 路径/运行中/routeSQL 断开/超时/正常回归)已注册 run-all;' + 'v2.52.0 性能服务化(P0-1 Worker 并行 JOIN + P0-3 HTTP WorkerPool):JoinEngine.parallelHashJoinWithWorker 把 Hash Join 探测(JOIN_PROBE)真实分发到 Worker 线程(主线程预计算键 + 浅拷贝行附加 __joinKey + execAll 分发 + 合并 + LEFT 补行 + 失败回退同步 _parallelHashJoin);Executor._performJoinAsync 委托 JoinExecutor 供异步路径使用;execSelectShardedAsync 支持单普通表等值 INNER/LEFT JOIN 的 Worker 并行(_canShardAsyncSelect 扩展 + WHERE 不得引用右表列的安全限制);DBServer 启动时初始化 WorkerPool(lazy,默认 CPU-1,可 enableWorkerPool=false 关闭)并在 stop 时终止,使 HTTP 服务中 JOIN/SORT/FILTER/GROUP_BY 并行算子真实可用;新增 worker-join.test.js 5 项(INNER/LEFT/无池回退/右表列回退/任务分发断言)已注册 run-all;' + 'v2.51.0 缺陷修复(物化视图注册为可查询关系 + WAL 大批次分块):物化视图作为只读关系注册到 Database(materializedViews 注册表 + hasTable/getTable 识别 + 只读表外观 + 命名冲突双向拒绝 + INFORMATION_SCHEMA.TABLES/COLUMNS 与 pg_class 可见 + CLI/.tables 与 HTTP 表列表纳入 + 持久化重建后重新注册 + SELECT/JOIN/EXPLAIN/跨库可解析),修复 CREATE MATERIALIZED VIEW 通过 SQL 调用时校验错误变为未处理 Promise 拒绝的问题(createMaterializedView 同步抛出 + execCreateMaterializedView 透传异步刷新);事务 WAL 按 maxEntriesPerBatch 分块,消除同步突发写入 >65535 条时 serializeBatch 的 RangeError 崩溃路径(守卫保留供防御测试);物化视图测试 +4 项(注册/冲突/只读/持久化),wal-count-bound 7/7 全绿;' + 'v2.50.0 T-05/T-06 架构清理:优化器 barrel 补齐 5 个生产可达类导出(ExplainVisualizer/JoinOrderOptimizer/QueryPlanCache/Histogram·HistogramBucket/TimestampQueryRewriter,经 engine/public-api.js 接主入口)+ StorageEngine 统一存储引擎接口(STORAGE_ENGINE_METHODS 契约 + isStorageEngine/assertStorageEngine 校验 + STORAGE_BACKENDS 四后端工厂 memory/file/binary/buffered + StorageEngine.switchBackend 运行时切换 + createStorageEngine)+ T-06 lockfree-worker.js 占位桩整体移除(lockfree-manager 移除 _startWorkers/useWorkers,单线程 COW 模式收敛);storage-engine.test 16 项(含绕过)+ optimizer-exports.test 4 项(含主入口接线)已注册 run-all,全量 806/806 全绿、幻影审计 A 类 0/B 类 0、dist 22/22 含新符号;' + 'v2.49.0 E34 数据血缘收尾(UPDATE 源追踪 + CREATE VIEW 事前环拒绝 + Mermaid 导出):extractUpdateColumnLineage(UPDATE SET 列级血缘,直接列/表达式/函数,常量不产生依赖,多表 UPDATE...FROM 无前缀 unresolved)+ recordStatement UpdateStatement 分支;wouldCreateCycle + DDLExecutor._assertNoLineageCycle(CREATE VIEW/CREATE MATERIALIZED VIEW 事前环拒绝,仅目标视图已存在时预检,保留 v2.11 E15 M2 T3 前向引用后补行为);toMermaid/getLineageMermaid(Mermaid flowchart 导出);生产入口导出 + lineage-e34 19 项 + 生产接线 4 项,run-all 804/804 全绿;' + 'v2.48.0 数据血缘全链路落地(P7 M1-M4 + P8 深化 + P9 版本历史):表级/列级血缘自动记录(CREATE VIEW/MATERIALIZED VIEW/INSERT...SELECT)、LINEAGE SQL 语句(LINEAGE TABLE/COLUMN + lineage-executor + MCP 工具)、任务级登记(CopyStream/Migrator)、持久化(meta v5 随 PersistedDatabase 落盘)、环检测 detectCycles、CTE 列级推导(collectCtes)、HTML 可视化 toHTML、版本历史(getLineageHistory/getLineageAtTime/getLineageEvolution)、变更影响分析(analyzeImpact),生产入口全导出 + 常驻测试 70 项;run-all 全量通过 + vitest 123/123 + c8 Lines 91.99%/Branches 88.43%;' + 'v2.45.2 双缺陷修复(多轮门禁盘点发现):① REGEXP (?g)/(?y) 全局/粘性标志剥离——编译产物跨行 LRU 缓存复用而 g/y 的 lastIndex 状态使求值行依赖((?g)a 四行 [1,0,1,0]),REGEXP 逐行独立求值语义下 g/y 无意义且有害,编译期剥离(i/m/s/u 保持);② WASM min/max 对齐区 NaN 传播——f32x4.min/max 按 IEEE 语义传播 NaN 与"NaN 被忽略"文档语义相悖(v2.45.1 激活 SIMD 路径后成为实际行为),NaN 结果 ⟺ 对齐区有 NaN lane,JS 重扫跳过 NaN 与降级路径对齐;绕过测试 v2450 +2(29 项)、wasm-simd-engine E3 守卫(23 项);' + 'v2.45.1 WASM SIMD 常量错位修复 + 覆盖率债务清零:f32x4_max 误写 0xEA(实为 f32x4.pmin)致 init 运行时验证恒败、引擎自引入即静默降级(SIMD 路径从未激活,全部流量走 TypedArray fallback)——修正 0xE9 并经验解码验证(0xE7=div/0xE8=min/0xE9=max/0xEA=pmin/0xEB=pmax),修复后 READY/WASM_SIMD 五算子精确正确;wasm-simd-engine 移出 c8 exclude 并补全路径测试(初始化/SIMD 数值/降级/扩容/单例 + 修复守卫);lockfree-worker.js 人工评估归档(确认占位桩:主路径不经 Worker,保持 exclude 豁免);' + 'v2.45.0 安满分诊 P1/P2 治理落地(eslint-security 58 errors 分诊第四节立项 ①②③④ 全部修复):REGEXP 用户正则防护(security/regex-guard.js compileUserRegex:模式长度上限 1024 + 嵌套量词灾难回溯编译期筛查,bitmap-filter _getRegex 接线)+ LIKE/GLOB/KEYS→regex 转换入口统一模式长度上限 1024(bitmap-filter _likeToRegex/_buildEscapeRegex、btree/offheap 部分索引 LikeExpr、cache-adapter KEYS、sqlite-kv-adapter LIKE、expression.js GLOB 共 7 处)+ oracle-adapter DECODE 线性解析重写(消除指数回溯,修复多分支 CASE 语义 + 括号/引号感知 + 嵌套 DECODE 递归转换)+ 输入长度上限(sql-injection-detector 256KB fail-closed、schema-versioning DDL 256KB + 顶层逗号线性切分);绕过测试 v2450-p0-bypass-boundary 通过;v2.0 长期路线图收官:v2.0-1..6 全部落地(JSONB 二进制 JSON / Lambda 表达式 x->x+1 / UNNEST 多列并行 / DEFERRABLE 约束 / EXCLUDE 排他约束 / LISTEN-NOTIFY 异步通知,DoD 5/5 各 27-39 用例);v2.0-7 WAL flush 慢 seq 初始化 flaky 修复 + wal-crash-validation 测试加固(10/10 稳定 + 绕过测试有效性验证)+ flush-timeout-simulation FT2/FT4 libuv 时钟边界修复;全量回归 run-all 459/459 全绿(历史首次);v2.0-8 SQL:2016 阶段1 聚合补全(PERCENTILE_CONT/DISC + STRING_AGG/ARRAY_AGG + MODE + BOOL_AND/OR/EVERY + WIDTH_BUCKET + REGR_*,DoD 5/5 37/37);v2.0-9 SQL:2016 阶段2 MATCH_RECOGNIZE 行模式识别(Part 2 §21.6 新执行器,DoD 5/5 30/30);v2.0-10 SQL:2016 阶段3 多态表函数 PTF(Part 2 §10.9:ANY TABLE/COLUMN/MAX(n) + RETURNS TABLE + 函数体 AST 重写 + 物化临时表 + JOIN 内调用 + 幂等性/零副作用,DoD 5/5 25/25,run-all 461/462 + audit-log A8 时间竞态 flaky 修复);v2.0-11 SQL:2016 阶段4 RATIO_TO_REPORT 分析函数(Part 2 §9.6 G9:分区求和比例 + NULL/除零语义 + 参数校验,DoD 5/5 14/14)+ 窗口函数源表零副作用 P0 修复(window-functions.js 浅拷贝行,156 项窗口回归全绿);v2.0-12 SQL:2016 阶段5 类型系统深化(Part 2 §4.4 G11 INTERVAL 完整类型:YEAR/MONTH/DAY/HOUR/MINUTE/SECOND 字段子句 + 单位内嵌 + 纯时间/天数自动判断 + DAY TO MINUTE HH:MM + DAY TO HOUR/MINUTE/SECOND 时间部分列宽 + date/timestamp±interval 日历月 clamp + 算术/比较/CAST/一元运算;G12 TIMESTAMP WITH TIME ZONE:带偏移解析→UTC 内部存储 + 会话时区显示 + SET TIME ZONE/SHOW TIME ZONE + INSERT/UPDATE 归一化 + 混合比较,DoD 5/5 62/62);v1.42.x 基础:STRUCT 类型 / LIST-ARRAY 完整操作 / WITHOUT ROWID 表 / VACUUM-REINDEX / user_version-application_id 持久化 + v1.42.1 全量回归闭环(16 个预存失败清零 + 2 个源码回归修复:query-isolation 原型链 / select-executor ORDER BY 聚合别名);v1.41.x 基础:ASOF JOIN / ATTACH DATABASE / COMMENT ON / PIVOT 多聚合 / WITH CHECK OPTION;v1.40.4 基础:PRAGMA 完整集(25+ SQLite 标准)+ 版本号多 SDK 单一来源同步;v1.40.3 基础:3 个 P0 安全修复真实现 + SECURITY DEFINER 真实现 + 循环依赖 0;v2.11(E15)遗留清零:WAL 恢复数据版本一致性 + 视图 lossy 根治 + 多视图 CASCADED 链 + SECURITY DEFINER 真实现 + RedisCacheAdapter 真实现 + Arrow 有限宽度 guard + WAL count 上界 + executor 拆分(sql-reconstructor);v2.12(E16)稳定性与分布式闭环:flaky 三件套根治(2pc-perf/index-build/soak-test + vector-executor 门槛统一)+ 跨库 DDL/事务 + WAL hot-standby 探针 + 分布式真实网络验证(raft/3PC/2PC + PK fastpath WAL 缺陷修复 + 2PC rejection 修复)+ executor PRAGMA 族拆离(pragma-executor,-550 行)+ 全量回归 612/612 + DL-D3 时序 flaky 修复;v2.13(E17)收尾治理与 SQL/XML:版本号全量对齐 + SQL:2016 阶段6 G10(XMLSERIALIZE/XMLAGG/XMLTABLE)+ 全量 c8 复验 + 跨库 TPC-H 数据刷新 + SQL:2023 roadmap 收敛;v2.14(E18)列存文件格式 M1+M2:PJDC 二进制列文件格式(表签名三层校验:schemaHash + rowCount + columnCount)+ save/load 持久化加速缓存 + 列存 roundtrip 持久化验证(22+8 用例全绿);v2.15(E19)列存文件格式 M3:WAL _invalidateTableIndexes 修复(checkpoint 后索引失效重建)+ run-all 618/618 全绿;v2.15(E20)工程化基线清零:parquet.js 3 处 setUint32 guard 补零 + 超限绕过测试 + 版本号同步 2.13.0→2.15.0 + c8 覆盖率实测 + 审计章节归档;';
|