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
package/src/executor/wal.js
CHANGED
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
import { Database, Table, ColumnDef } from './database.js';
|
|
54
54
|
import { MemoryKVAdapter } from '../storage/kv-adapter.js';
|
|
55
55
|
import { computeCRC32, assertSafeSeq } from '../utils/wal-common.js';
|
|
56
|
+
import crypto from 'node:crypto';
|
|
56
57
|
|
|
57
58
|
const WAL_KEY_PREFIX = 'db:wal';
|
|
58
59
|
const CP_KEY_PREFIX = 'db:checkpoint';
|
|
@@ -98,6 +99,18 @@ function hexToBytes(hex) {
|
|
|
98
99
|
return bytes;
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
/**
|
|
103
|
+
* 计算十六进制字符串的 HMAC-SHA256(2026-10-31 渗透测试 M-03 修复)
|
|
104
|
+
* @param {string} valueHex - 待认证的十六进制内容
|
|
105
|
+
* @param {string} key - HMAC 密钥
|
|
106
|
+
* @returns {string} 十六进制 HMAC
|
|
107
|
+
*/
|
|
108
|
+
function hmacHex(valueHex, key) {
|
|
109
|
+
return crypto.createHmac('sha256', Buffer.from(key, 'utf8'))
|
|
110
|
+
.update(valueHex, 'utf8')
|
|
111
|
+
.digest('hex');
|
|
112
|
+
}
|
|
113
|
+
|
|
101
114
|
// ═══════════════════════════════════════════
|
|
102
115
|
// 二进制 WAL 批次格式
|
|
103
116
|
// ═══════════════════════════════════════════
|
|
@@ -143,9 +156,10 @@ const OP_TO_NAME = {
|
|
|
143
156
|
/**
|
|
144
157
|
* 将 WAL 条目数组序列化为二进制批次
|
|
145
158
|
* @param {Object[]} entries
|
|
146
|
-
* @
|
|
159
|
+
* @param {string} [hmacKey] - 可选 HMAC 密钥;设置后返回 `${hex}.${hmac}` 后缀
|
|
160
|
+
* @returns {string} 十六进制字符串(配置 hmacKey 时含 `.hmac` 后缀)
|
|
147
161
|
*/
|
|
148
|
-
function serializeBatch(entries) {
|
|
162
|
+
function serializeBatch(entries, hmacKey) {
|
|
149
163
|
// P1-F 修复 v1.34.2:批次条目数 >65535 时旧格式(count 2B)会静默截断
|
|
150
164
|
// 抛出错误而非截断,让调用方感知 bug(groupCommitMaxEntries 默认 64,不应触发)
|
|
151
165
|
// 注意:上层 _flushBatch 已按 MAX_ENTRIES_PER_BATCH 拆分,正常不应触达此守卫。
|
|
@@ -238,7 +252,11 @@ function serializeBatch(entries) {
|
|
|
238
252
|
const crc = crc32(crcBuf);
|
|
239
253
|
fullView.setUint32(8, crc, false);
|
|
240
254
|
|
|
241
|
-
|
|
255
|
+
const hex = bytesToHex(fullBuf);
|
|
256
|
+
if (hmacKey) {
|
|
257
|
+
return `${hex}.${hmacHex(hex, hmacKey)}`;
|
|
258
|
+
}
|
|
259
|
+
return hex;
|
|
242
260
|
}
|
|
243
261
|
|
|
244
262
|
/**
|
|
@@ -246,11 +264,36 @@ function serializeBatch(entries) {
|
|
|
246
264
|
* @param {string} hex
|
|
247
265
|
* @param {number} batchSeq
|
|
248
266
|
* @param {function} [reportWarn] - 可选,告警回调 ({ level, message, context })
|
|
267
|
+
* @param {string} [hmacKey] - 可选 HMAC 密钥;设置后校验批次 HMAC(不匹配返回 null)
|
|
249
268
|
* @returns {{ seq: number, entries: Object[] }|null}
|
|
250
269
|
*/
|
|
251
|
-
function deserializeBatch(hex, batchSeq, reportWarn) {
|
|
270
|
+
function deserializeBatch(hex, batchSeq, reportWarn, hmacKey) {
|
|
252
271
|
try {
|
|
253
|
-
|
|
272
|
+
// 2026-10-31 渗透测试 M-03 修复:分离并校验 HMAC 后缀(`.hmacHex`)
|
|
273
|
+
let batchHex = hex;
|
|
274
|
+
let storedHmac = null;
|
|
275
|
+
const dotIdx = hex.indexOf('.');
|
|
276
|
+
if (dotIdx !== -1) {
|
|
277
|
+
batchHex = hex.slice(0, dotIdx);
|
|
278
|
+
storedHmac = hex.slice(dotIdx + 1);
|
|
279
|
+
}
|
|
280
|
+
if (hmacKey) {
|
|
281
|
+
if (storedHmac) {
|
|
282
|
+
const expectedHmac = hmacHex(batchHex, hmacKey);
|
|
283
|
+
if (expectedHmac !== storedHmac) {
|
|
284
|
+
const warnMsg = 'Batch HMAC mismatch(WAL 可能被篡改)';
|
|
285
|
+
if (reportWarn) reportWarn('warn', warnMsg);
|
|
286
|
+
else console.warn(`[WAL] ${warnMsg}`);
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
} else {
|
|
290
|
+
// 旧格式(无 HMAC 后缀):向后兼容放行,但提示未受认证完整性保护
|
|
291
|
+
const warnMsg = 'Batch 缺少 HMAC(旧格式 WAL,未受认证完整性保护)';
|
|
292
|
+
if (reportWarn) reportWarn('warn', warnMsg);
|
|
293
|
+
else console.warn(`[WAL] ${warnMsg}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const buf = hexToBytes(batchHex);
|
|
254
297
|
if (buf.length < BATCH_HEADER_SIZE) {
|
|
255
298
|
return null;
|
|
256
299
|
}
|
|
@@ -350,6 +393,12 @@ export class WriteAheadLog {
|
|
|
350
393
|
this._seqInitialized = false; // 懒初始化标记
|
|
351
394
|
this._seq = 0;
|
|
352
395
|
|
|
396
|
+
// 2026-10-31 渗透测试 M-03 修复:可选 WAL/checkpoint 认证完整性密钥。
|
|
397
|
+
// 设置 hmacKey 后,新写入的二进制批次与 checkpoint 均附加 HMAC-SHA256;
|
|
398
|
+
// 恢复时校验 HMAC(不匹配则拒绝该批次/快照)。旧格式(无 HMAC)仍可读取
|
|
399
|
+
// (向后兼容),但会告警提示。
|
|
400
|
+
this._hmacKey = options.hmacKey || null;
|
|
401
|
+
|
|
353
402
|
// ── 历史 checkpoint 保留策略(P3-#17) ──
|
|
354
403
|
/** @type {boolean} 保留所有历史 checkpoint(时间旅行查询必需) */
|
|
355
404
|
this._keepAllCps = options.keepAllCheckpoints === true;
|
|
@@ -572,7 +621,7 @@ export class WriteAheadLog {
|
|
|
572
621
|
continue;
|
|
573
622
|
}
|
|
574
623
|
|
|
575
|
-
const result = deserializeBatch(raw, batchSeq, this._reportError.bind(this));
|
|
624
|
+
const result = deserializeBatch(raw, batchSeq, this._reportError.bind(this), this._hmacKey);
|
|
576
625
|
if (!result || !result.entries) {
|
|
577
626
|
continue;
|
|
578
627
|
}
|
|
@@ -866,9 +915,16 @@ export class WriteAheadLog {
|
|
|
866
915
|
const cpKey = `${CP_KEY_PREFIX}:${this.dbName}:${String(seq).padStart(16, '0')}`;
|
|
867
916
|
const tmpKey = cpKey + ':tmp'; // 临时键,用于原子切换
|
|
868
917
|
|
|
918
|
+
// 2026-10-31 渗透测试 M-03 修复:配置 hmacKey 时为 checkpoint 附加 HMAC 认证
|
|
919
|
+
let cpJson = JSON.stringify(cpData);
|
|
920
|
+
if (this._hmacKey) {
|
|
921
|
+
const hmac = hmacHex(cpJson, this._hmacKey);
|
|
922
|
+
cpJson = JSON.stringify({ ...cpData, _hmac: hmac });
|
|
923
|
+
}
|
|
924
|
+
|
|
869
925
|
try {
|
|
870
926
|
// 阶段 1:写入临时 checkpoint
|
|
871
|
-
await this.kv.set(tmpKey,
|
|
927
|
+
await this.kv.set(tmpKey, cpJson);
|
|
872
928
|
if (typeof this.kv.flush === 'function') {
|
|
873
929
|
await this.kv.flush();
|
|
874
930
|
}
|
|
@@ -1128,7 +1184,7 @@ export class WriteAheadLog {
|
|
|
1128
1184
|
for (let offset = 0; offset < entries.length; offset += chunkSize) {
|
|
1129
1185
|
const chunk = entries.slice(offset, offset + chunkSize);
|
|
1130
1186
|
const chunkStartSeq = startSeq + offset;
|
|
1131
|
-
const hex = serializeBatch(chunk);
|
|
1187
|
+
const hex = serializeBatch(chunk, this._hmacKey);
|
|
1132
1188
|
const key = `${WAL_KEY_PREFIX}:${this.dbName}:seq:${String(chunkStartSeq).padStart(16, '0')}`;
|
|
1133
1189
|
await this.kv.set(key, hex);
|
|
1134
1190
|
}
|
|
@@ -1268,10 +1324,10 @@ export class WriteAheadLog {
|
|
|
1268
1324
|
continue;
|
|
1269
1325
|
}
|
|
1270
1326
|
|
|
1271
|
-
//
|
|
1272
|
-
if (/^[0-9a-f]
|
|
1327
|
+
// 尝试二进制批次格式(十六进制字符串;2026-10-31 起可带 `.hmac` 认证后缀)
|
|
1328
|
+
if (/^[0-9a-f]+(\.[0-9a-f]+)?$/i.test(raw) && raw.length > 20) {
|
|
1273
1329
|
const keySeq = parseInt(key.split(':').pop(), 10) || 0;
|
|
1274
|
-
const result = deserializeBatch(raw, keySeq, this._reportError.bind(this));
|
|
1330
|
+
const result = deserializeBatch(raw, keySeq, this._reportError.bind(this), this._hmacKey);
|
|
1275
1331
|
if (result && result.entries) {
|
|
1276
1332
|
for (const entry of result.entries) {
|
|
1277
1333
|
if (entry.seq > afterSeq) {
|
|
@@ -1331,6 +1387,20 @@ export class WriteAheadLog {
|
|
|
1331
1387
|
return 0;
|
|
1332
1388
|
}
|
|
1333
1389
|
|
|
1390
|
+
// 2026-10-31 渗透测试 M-03 修复:校验 checkpoint HMAC(若配置密钥)
|
|
1391
|
+
if (this._hmacKey) {
|
|
1392
|
+
if (cpData && typeof cpData === 'object' && '_hmac' in cpData) {
|
|
1393
|
+
const { _hmac, ...payloadObj } = cpData;
|
|
1394
|
+
const expectedHmac = hmacHex(JSON.stringify(payloadObj), this._hmacKey);
|
|
1395
|
+
if (expectedHmac !== _hmac) {
|
|
1396
|
+
this._reportError('warn', `_loadCheckpoint: checkpoint HMAC mismatch (可能被篡改), skipping "${latestKey}"`);
|
|
1397
|
+
return 0;
|
|
1398
|
+
}
|
|
1399
|
+
} else {
|
|
1400
|
+
this._reportError('warn', `_loadCheckpoint: checkpoint 缺少 HMAC(旧格式快照,未受认证完整性保护)`);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1334
1404
|
// 还原表
|
|
1335
1405
|
for (const [tableName, tableMeta] of Object.entries(cpData.tables)) {
|
|
1336
1406
|
const colDefs = tableMeta.columns.map(c =>
|
package/src/graphql-bridge.js
CHANGED
|
@@ -337,9 +337,50 @@ class Parser {
|
|
|
337
337
|
export class GraphQLBridge {
|
|
338
338
|
/**
|
|
339
339
|
* @param {object} executor - Executor 实例
|
|
340
|
+
* @param {object} [options]
|
|
341
|
+
* @param {number} [options.maxDepth=8] - 最大查询嵌套深度(防深度炸弹)
|
|
342
|
+
* @param {number} [options.maxSelections=100] - 单请求最大字段/选择数(防字段数放大)
|
|
340
343
|
*/
|
|
341
|
-
constructor(executor) {
|
|
344
|
+
constructor(executor, options = {}) {
|
|
342
345
|
this.executor = executor;
|
|
346
|
+
this.maxDepth = Number.isFinite(options.maxDepth) ? options.maxDepth : 8;
|
|
347
|
+
this.maxSelections = Number.isFinite(options.maxSelections) ? options.maxSelections : 100;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* 校验查询成本(2026-10-31 渗透测试 R7 修复)
|
|
352
|
+
*
|
|
353
|
+
* 递归遍历 AST,统计最大嵌套深度与总选择(字段)数。
|
|
354
|
+
* 超限直接抛错,防止深度嵌套/海量字段的解析与执行放大 DoS。
|
|
355
|
+
* @param {{operation: string, selections: Array}} doc - parseDocument 返回的 AST
|
|
356
|
+
* @private
|
|
357
|
+
*/
|
|
358
|
+
_validateQueryCost(doc) {
|
|
359
|
+
let totalSelections = 0;
|
|
360
|
+
let maxDepth = 0;
|
|
361
|
+
|
|
362
|
+
const walk = (fields, depth) => {
|
|
363
|
+
if (depth > maxDepth) {
|
|
364
|
+
maxDepth = depth;
|
|
365
|
+
}
|
|
366
|
+
if (!fields || fields.length === 0) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
for (const f of fields) {
|
|
370
|
+
totalSelections++;
|
|
371
|
+
if (totalSelections > this.maxSelections) {
|
|
372
|
+
throw new Error(`GraphQL 查询字段数超过上限 ${this.maxSelections}(防资源耗尽)`);
|
|
373
|
+
}
|
|
374
|
+
if (f.fields && f.fields.length > 0) {
|
|
375
|
+
walk(f.fields, depth + 1);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
walk(doc.selections, 1);
|
|
381
|
+
if (maxDepth > this.maxDepth) {
|
|
382
|
+
throw new Error(`GraphQL 查询深度 ${maxDepth} 超过上限 ${this.maxDepth}(防嵌套 DoS)`);
|
|
383
|
+
}
|
|
343
384
|
}
|
|
344
385
|
|
|
345
386
|
/**
|
|
@@ -456,6 +497,9 @@ ${mutationFields.join('\n')}
|
|
|
456
497
|
const parser = new Parser(tokens);
|
|
457
498
|
const doc = parser.parseDocument();
|
|
458
499
|
|
|
500
|
+
// 2026-10-31 渗透测试 R7 修复:查询成本上限(深度/字段数)
|
|
501
|
+
this._validateQueryCost(doc);
|
|
502
|
+
|
|
459
503
|
if (doc.operation === 'mutation') {
|
|
460
504
|
return this._executeMutations(doc.selections);
|
|
461
505
|
}
|
package/src/http-api.js
CHANGED
|
@@ -207,6 +207,16 @@ function _containsSubquery(expr) {
|
|
|
207
207
|
*/
|
|
208
208
|
function parseBody(req, limit = 1048576) {
|
|
209
209
|
return new Promise((resolve, reject) => {
|
|
210
|
+
// 2026-10-31 渗透测试 H-03 修复:拒绝非 JSON Content-Type(消除跨站盲写 SQL 面),
|
|
211
|
+
// 与 http-server/server.js parseBody 行为对齐。
|
|
212
|
+
const contentType = (req.headers['content-type'] || '').toLowerCase();
|
|
213
|
+
if (contentType && !contentType.includes('application/json')) {
|
|
214
|
+
// 排空 body 但不销毁连接,让 handler 正常返回 400 错误响应
|
|
215
|
+
req.removeAllListeners('data');
|
|
216
|
+
req.resume();
|
|
217
|
+
reject(new Error('不支持的 Content-Type:仅接受 application/json'));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
210
220
|
let body = '';
|
|
211
221
|
let totalBytes = 0;
|
|
212
222
|
req.on('data', chunk => {
|
|
@@ -229,6 +239,26 @@ function parseBody(req, limit = 1048576) {
|
|
|
229
239
|
});
|
|
230
240
|
}
|
|
231
241
|
|
|
242
|
+
/**
|
|
243
|
+
* 认证端点安全错误文案(2026-10-31 渗透测试 L-05 修复)
|
|
244
|
+
*
|
|
245
|
+
* AuthManager 已把所有可预期失败归一为安全文案(反用户枚举);
|
|
246
|
+
* 但非预期异常(数据库故障、TypeError 等)可能携带内部细节。
|
|
247
|
+
* 这里仅透出白名单内的业务文案,其余统一为通用信息(dev 模式保留细节便于排查)。
|
|
248
|
+
* @param {Error} err
|
|
249
|
+
* @returns {string}
|
|
250
|
+
*/
|
|
251
|
+
function safeAuthError(err) {
|
|
252
|
+
const msg = err?.message || '';
|
|
253
|
+
const isDev = process.env.NODE_ENV === 'development';
|
|
254
|
+
if (msg === '注册失败' || msg === '用户名或密码错误' ||
|
|
255
|
+
msg === 'username 与 password 不能为空' || msg === '用户不存在' ||
|
|
256
|
+
msg === '旧密码错误') {
|
|
257
|
+
return msg;
|
|
258
|
+
}
|
|
259
|
+
return isDev ? msg : '认证服务异常,请稍后重试';
|
|
260
|
+
}
|
|
261
|
+
|
|
232
262
|
/**
|
|
233
263
|
* 统一错误响应(2026-08-14 M-06 修复:默认隐藏内部错误细节,防信息泄露)
|
|
234
264
|
* - NODE_ENV=development 时保留 err.message 便于调试
|
|
@@ -924,7 +954,7 @@ export class HttpApiServer {
|
|
|
924
954
|
res.end(JSON.stringify(result));
|
|
925
955
|
} catch (err) {
|
|
926
956
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
927
|
-
res.end(JSON.stringify({ error: err
|
|
957
|
+
res.end(JSON.stringify({ error: safeAuthError(err) }));
|
|
928
958
|
}
|
|
929
959
|
}
|
|
930
960
|
|
|
@@ -955,7 +985,7 @@ export class HttpApiServer {
|
|
|
955
985
|
}
|
|
956
986
|
} catch (err) {
|
|
957
987
|
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
958
|
-
res.end(JSON.stringify({ error: err
|
|
988
|
+
res.end(JSON.stringify({ error: safeAuthError(err) }));
|
|
959
989
|
}
|
|
960
990
|
}
|
|
961
991
|
|
package/src/http-server/index.js
CHANGED
|
@@ -32,6 +32,7 @@ export { DBServer };
|
|
|
32
32
|
* @param {string} [options.signingKey] - RSA 私钥 PEM(v1.12.0 数字签名)
|
|
33
33
|
* @param {string} [options.verifyKey] - RSA 公钥 PEM(v1.12.0 签名验证)
|
|
34
34
|
* @param {string} [options.auditFilePath] - 审计日志持久化文件路径
|
|
35
|
+
* @param {string} [options.anchorSigningKey] - 持久化审计 anchor HMAC 密钥(≥32 字节;配置 auditFilePath 时必填,M-5/fail-closed)
|
|
35
36
|
* @param {boolean} [options.enableAuditOnStart=true] - 启动时自动启用审计日志
|
|
36
37
|
* @param {string[]} [options.corsOrigins] - v1.34.6: CORS 允许的 Origin 白名单(null 或含 '*' 表示允许任意)
|
|
37
38
|
* @param {boolean} [options.trustUserIdHeader=false] - v1.34.6: 是否信任 X-User-Id 头部作为身份来源(默认 false,仅受信反代场景开启)
|
|
@@ -79,6 +80,7 @@ export async function startHTTPServer(executor, options = {}) {
|
|
|
79
80
|
signingKey: options.signingKey,
|
|
80
81
|
verifyKey: options.verifyKey,
|
|
81
82
|
auditFilePath: options.auditFilePath,
|
|
83
|
+
anchorSigningKey: options.anchorSigningKey,
|
|
82
84
|
enableAuditOnStart: options.enableAuditOnStart,
|
|
83
85
|
corsOrigins: options.corsOrigins,
|
|
84
86
|
trustUserIdHeader: options.trustUserIdHeader,
|
|
@@ -304,6 +304,19 @@ export function parseBody(req, limit = DEFAULT_BODY_LIMIT) {
|
|
|
304
304
|
const chunks = [];
|
|
305
305
|
let totalBytes = 0;
|
|
306
306
|
|
|
307
|
+
// 2026-10-31 渗透测试 H-03 修复:拒绝非 JSON Content-Type。
|
|
308
|
+
// 浏览器跨站"简单请求"(text/plain 等)此前可带 JSON body 盲执行 SQL;
|
|
309
|
+
// 现在仅接受 application/json,浏览器跨站 application/json 会触发 CORS preflight
|
|
310
|
+
// (默认被 CORS 'null' 策略拒绝),从而消除 CSRF 盲写面。
|
|
311
|
+
const contentType = (req.headers['content-type'] || '').toLowerCase();
|
|
312
|
+
if (contentType && !contentType.includes('application/json')) {
|
|
313
|
+
// 排空 body 但不销毁连接,让 handler 正常返回 400 错误响应
|
|
314
|
+
req.removeAllListeners('data');
|
|
315
|
+
req.resume();
|
|
316
|
+
reject(new Error('不支持的 Content-Type:仅接受 application/json'));
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
307
320
|
req.on('data', chunk => {
|
|
308
321
|
totalBytes += chunk.length;
|
|
309
322
|
if (totalBytes > limit) {
|
|
@@ -657,14 +657,18 @@ export class WSServer {
|
|
|
657
657
|
|
|
658
658
|
/**
|
|
659
659
|
* Origin 校验(M-04 修复):配置白名单后,浏览器 Origin 必须精确匹配
|
|
660
|
+
* 2026-10-31 渗透测试 L-03 收紧扣紧:未配置白名单时(零认证回环模式),
|
|
661
|
+
* 仍拒绝携带 Origin 的浏览器连接(跨站 WebSocket 劫持面),仅放行无 Origin
|
|
662
|
+
* 的非浏览器客户端(curl/Node SDK)。
|
|
660
663
|
* @param {string|null} origin
|
|
661
664
|
* @returns {boolean}
|
|
662
665
|
* @private
|
|
663
666
|
*/
|
|
664
667
|
_checkOrigin(origin) {
|
|
665
668
|
if (!this._allowedOrigins) {
|
|
666
|
-
|
|
667
|
-
|
|
669
|
+
// 未配置白名单:仅允许无 Origin 的非浏览器客户端(浏览器必然携带 Origin)
|
|
670
|
+
return !origin;
|
|
671
|
+
}
|
|
668
672
|
if (!origin) {
|
|
669
673
|
return this._allowNoOrigin;
|
|
670
674
|
} // 无 Origin:非浏览器客户端
|
|
@@ -292,6 +292,8 @@ export class ReplicaHttpClient {
|
|
|
292
292
|
allowedPorts: [],
|
|
293
293
|
};
|
|
294
294
|
this._ssrfGuard = null;
|
|
295
|
+
// 2026-10-31 渗透测试 R6 修复:DNS 钉扎(可选,默认关闭)
|
|
296
|
+
this._pinDns = options.pinDns === true;
|
|
295
297
|
}
|
|
296
298
|
|
|
297
299
|
/**
|
|
@@ -347,11 +349,11 @@ export class ReplicaHttpClient {
|
|
|
347
349
|
}
|
|
348
350
|
|
|
349
351
|
async _request(method, path, request) {
|
|
350
|
-
// v1.34.5: SSRF
|
|
352
|
+
// v1.34.5: SSRF 防护校验主节点地址(2026-10-31 M-04:升级为静态+DNS 深度校验)
|
|
351
353
|
const fullUrl = `http://${this.masterHost}:${this.masterPort}${path}`;
|
|
352
354
|
const guard = await this._getSSRFGuard();
|
|
353
355
|
if (guard) {
|
|
354
|
-
const check = guard.
|
|
356
|
+
const check = await guard.validateDeep(fullUrl);
|
|
355
357
|
if (!check.allowed) {
|
|
356
358
|
throw new Error(`SSRF 阻止主节点请求: ${check.reason}`);
|
|
357
359
|
}
|
|
@@ -362,6 +364,12 @@ export class ReplicaHttpClient {
|
|
|
362
364
|
body = JSON.stringify(request);
|
|
363
365
|
}
|
|
364
366
|
|
|
367
|
+
// 2026-10-31 渗透测试 R6 修复:DNS 钉扎(可选)——用解析 IP 直连,保留 Host 头
|
|
368
|
+
let pinned = null;
|
|
369
|
+
if (this._pinDns && guard) {
|
|
370
|
+
pinned = await guard.resolvePinnedHost(fullUrl);
|
|
371
|
+
}
|
|
372
|
+
|
|
365
373
|
const headers = {
|
|
366
374
|
'Content-Type': 'application/json',
|
|
367
375
|
};
|
|
@@ -369,10 +377,14 @@ export class ReplicaHttpClient {
|
|
|
369
377
|
if (this._authToken) {
|
|
370
378
|
headers['Authorization'] = `Bearer ${this._authToken}`;
|
|
371
379
|
}
|
|
380
|
+
// DNS 钉扎时保留原始 Host 头
|
|
381
|
+
if (pinned) {
|
|
382
|
+
headers.Host = pinned.hostHeader;
|
|
383
|
+
}
|
|
372
384
|
|
|
373
385
|
const reqOpts = {
|
|
374
|
-
hostname: this.masterHost,
|
|
375
|
-
port: this.masterPort,
|
|
386
|
+
hostname: pinned ? pinned.ip : this.masterHost,
|
|
387
|
+
port: pinned ? pinned.port : this.masterPort,
|
|
376
388
|
path,
|
|
377
389
|
method,
|
|
378
390
|
headers,
|
package/src/sdk/java/pom.xml
CHANGED
|
@@ -62,7 +62,7 @@ import java.util.NoSuchElementException;
|
|
|
62
62
|
public class PureClient {
|
|
63
63
|
|
|
64
64
|
/** SDK 版本号。 */
|
|
65
|
-
public static final String SDK_VERSION = "2.53.
|
|
65
|
+
public static final String SDK_VERSION = "2.53.1";
|
|
66
66
|
|
|
67
67
|
/** 默认请求超时(秒)。 */
|
|
68
68
|
public static final long DEFAULT_TIMEOUT_SECONDS = 30;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "pure-js-db/php-sdk",
|
|
3
3
|
"description": "PHP SDK for pure-js-db — pure JavaScript database engine, via HTTP RESTful API.",
|
|
4
4
|
"type": "library",
|
|
5
|
-
"version": "2.53.
|
|
5
|
+
"version": "2.53.1",
|
|
6
6
|
"keywords": ["database", "sdk", "pure-js-db", "rest", "curl"],
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"require": {
|
package/src/sdk/rust/Cargo.toml
CHANGED
|
@@ -29,8 +29,8 @@ grep -E 'SDK 版本:\*\*[0-9]+\.[0-9]+\.[0-9]+\*\*' README.md # 期望: SDK
|
|
|
29
29
|
|
|
30
30
|
| 文件 | 当前值 |
|
|
31
31
|
|------|--------|
|
|
32
|
-
| `pure-js-db/package.json` | `2.53.
|
|
33
|
-
| `pure-js-db/src/index.js` `VERSION` | `'2.53.
|
|
32
|
+
| `pure-js-db/package.json` | `2.53.1` |
|
|
33
|
+
| `pure-js-db/src/index.js` `VERSION` | `'2.53.1'` |
|
|
34
34
|
|
|
35
35
|
---
|
|
36
36
|
|
package/src/sdk/rust/src/lib.rs
CHANGED