pure-js-db 2.53.0 → 2.53.2

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 CHANGED
@@ -3,11 +3,11 @@
3
3
  > **零依赖嵌入式数据库引擎 —— 单文件部署,同时支持向量搜索、MVCC 并发写入、CDC 变更捕获。**
4
4
  > 如果你的 Node.js 应用需要 PostgreSQL + Milvus + Debezium 的能力但不想要三个独立服务,这里一个文件就够。
5
5
 
6
- > 📌 **当前状态**:v2.53.0,成熟度 **Beta+(工程验证成熟)**。历史版本公告(E26 查询缓存/时间旅行、F1-F10 接线、安全审计加固等)详见 [CHANGELOG.md](CHANGELOG.md) 与 [docs/internal/audit/](docs/internal/audit/) 审计档案。安全模块默认 fail-closed,生产部署必须显式配置认证/RBAC/加密(见 [SECURITY.md](SECURITY.md) §2.2)。
6
+ > 📌 **当前状态**:v2.53.2,成熟度 **Beta+(工程验证成熟)**。历史版本公告(E26 查询缓存/时间旅行、F1-F10 接线、安全审计加固等)详见 [CHANGELOG.md](CHANGELOG.md) 与 [docs/internal/audit/](docs/internal/audit/) 审计档案。安全模块默认 fail-closed,生产部署必须显式配置认证/RBAC/加密(见 [SECURITY.md](SECURITY.md) §2.2)。
7
7
 
8
8
  ![Node.js](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen)
9
9
  ![License](https://img.shields.io/badge/license-MIT-blue)
10
- ![Version](https://img.shields.io/badge/version-2.53.0-blue)
10
+ ![Version](https://img.shields.io/badge/version-2.53.2-blue)
11
11
  ![Tests](https://img.shields.io/badge/tests-826%20files-brightgreen)
12
12
  ![LOC](https://img.shields.io/badge/loc-~225k-blue)
13
13
  ![Maturity](https://img.shields.io/badge/maturity-Beta%2B%20engineering%20validated-orange)
@@ -121,6 +121,8 @@ SELECT * FROM cdc_events WHERE table_name = 'orders' AND op = 'C';
121
121
 
122
122
  内置 MCP Server,AI 工具(Cursor / Claude Code / Claude Desktop)一键直连。
123
123
 
124
+ > 💡 **新手教程**:📘《用 Pure-JS-DB 给 Claude 一个本地记忆库》— [docs/tutorials/ai-agent-local-memory.md](docs/tutorials/ai-agent-local-memory.md)(MCP 安装 / Claude Desktop 配置 / 可复现示例,全程 3 步跑通建表+写入+查询)。
125
+
124
126
  **方式 A:CLI 一键启动(推荐)**
125
127
 
126
128
  ```bash
@@ -385,6 +387,7 @@ npm install pure-js-db
385
387
  | `src/serializer/serializer.js` | 列式序列化/反序列化,自动分页 |
386
388
  | `src/storage/kv-adapter.js` | Memory / LocalStorage / WeChat(miniprogram) 三种 KV 适配器 |
387
389
  | `src/storage/persisted-db.js` | 继承 Database,脏表标记增量保存,索引持久化 |
390
+ | `src/fdw/fdw-manager.js` | ⚠️ **实验性功能,当前未接线**:`CREATE FOREIGN TABLE ... USING csv/json/mysql/postgres` 仅注册包装器,`SELECT` **不会**路由到外部数据源(查询会报"表不存在")。**不要依赖该语法读取文件或连接外部数据库**(2026-10-31 渗透测试确认,避免误用导致安全假象) |
388
391
 
389
392
  ### 向量搜索(~2,500+ 行)
390
393
 
@@ -476,6 +479,7 @@ npm install pure-js-db
476
479
  | 🏗️ 架构师 / 决策者 | [COMPARISON 目录](docs/comparison/) — 项目成熟度深度评估 + 主流数据库对比 |
477
480
  | 📖 查 API 细节 | [API-REFERENCE.md](docs/API-REFERENCE.md) — 66 章节 API 参考手册 |
478
481
  | 🤖 AI 集成 | [AI-FEATURES-GUIDE.md](docs/AI-FEATURES-GUIDE.md) — MCP Server / AI_FUNCTION / Text-to-SQL |
482
+ | 🧪 AI Agent 实战教程 | [本地记忆库(MCP)](docs/tutorials/ai-agent-local-memory.md) · [本地语义搜索(向量 RAG)](docs/tutorials/ai-agent-vector-rag.md) · [知识图谱(SQL:2023 PGQ)](docs/tutorials/ai-agent-graph-pgq.md) — 三步让 AI 拥有本地持久记忆/检索/图谱;示例可直接运行 `examples/ai-agent-*.mjs` |
479
483
  | 🔧 架构理解 | [ARCHITECTURE.md](ARCHITECTURE.md) |
480
484
  | 🔒 安全 / 合规 | [SECURITY.md](SECURITY.md) — SOC2/ISO27001 合规路线图 + 17 项安全控制 + VDP 漏洞披露 + [USER-GUIDE §14 安全](docs/USER-GUIDE.md#14-安全rbac--加密--sql-防火墙) + [CVE-RESPONSE-PROCESS.md](docs/CVE-RESPONSE-PROCESS.md) |
481
485
  | 🤝 社区贡献 | [CONTRIBUTING.md](CONTRIBUTING.md) — 12 章节贡献指南 + [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) — Contributor Covenant 2.1 |
@@ -601,6 +605,14 @@ Pure-JS-DB 架构:
601
605
  - NAPI 调用开销 (~300ns) 在频繁调用中抵消 SIMD 收益
602
606
  - 核心引擎的解析/执行/存储/事务仍是 JS 瓶颈
603
607
 
608
+ ### native/index.node 的来源与可复现构建(供应链透明度)
609
+
610
+ > 2026-10-31 渗透测试 L-04 补充:npm 发布包内的 `native/index.node` 是**预编译 napi 二进制**,无法从包内直接审计源码。供应链审计建议如下:
611
+ >
612
+ > - **来源**:由仓库 `native/` 下的 Rust 源码构建(`npm run prebuild-native` → `scripts/prebuild-native.cjs`),源码随仓库发布,可审计。
613
+ > - **可复现构建**:在安装 Rust 工具链(`cargo`)与 Node 的干净环境执行 `npm run prebuild-native`,应产出相同功能的 `native/index.node`;建议在 CI 中记录产物 **SHA-256** 并与发布物比对。
614
+ > - **安全回退**:未配置/缺失 `native/index.node` 时 `isNativeAvailable()` 返回 `false`,数据库功能完整可用(纯 JS 回退),不影响正确性——若无法审计预编译产物,可直接删除该文件以强制纯 JS 模式。
615
+
604
616
  ### Pure-JS-DB 的核心价值
605
617
 
606
618
  **不是性能,而是部署灵活性和零依赖**:
@@ -687,7 +699,7 @@ const backend = getAccelerationBackend();
687
699
 
688
700
  ### Rust NAPI 加速引擎性能 (SIMD)
689
701
 
690
- > ⚠️ **2026-10-27 独立评测修正(2026-10-28 部分已修复)**:下表数据来自 v1.32 时代的旧基准。独立评测实测发现:Rust 加速层多数路径未在生产代码接线(4/5 性能声称失效);评测同时暴露三处 AVX2 正确性 bug(utf8 批量编码、Float64 min/max 全 NaN、null bitmap ≥0x80),该三处已在 2026-10-28 修复并重建 `native/index.node`、相关 Native 路径已恢复。下表加速比仅供参考,不构成当前版本(v2.53.0)的性能承诺。
702
+ > ⚠️ **2026-10-27 独立评测修正(2026-10-28 部分已修复)**:下表数据来自 v1.32 时代的旧基准。独立评测实测发现:Rust 加速层多数路径未在生产代码接线(4/5 性能声称失效);评测同时暴露三处 AVX2 正确性 bug(utf8 批量编码、Float64 min/max 全 NaN、null bitmap ≥0x80),该三处已在 2026-10-28 修复并重建 `native/index.node`、相关 Native 路径已恢复。下表加速比仅供参考,不构成当前版本(v2.53.2)的性能承诺。
691
703
  >
692
704
  > 详见 [SIMD 优化报告](docs/simd/simd-final-optimization-report.md)
693
705
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pure-js-db",
3
- "version": "2.53.0",
3
+ "version": "2.53.2",
4
4
  "description": "Pure JavaScript 关系型数据库引擎 - 面向小程序/受限运行时环境。零 C/C++/WASM 依赖,支持 SQL 解析、B+树索引、查询优化、物化视图、数据分片、分布式事务、Worker Pool 并行、SharedArrayBuffer 共享内存、HTAP 混合存储、多语言客户端 SDK。可选 Rust NAPI 原生加速层(编译后性能提升 2-5x)。",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -70,6 +70,15 @@
70
70
  "test:bundle": "node test/bundle/bundle-verify.test.js",
71
71
  "prepublishOnly": "npm run prepublish-check && node scripts/prebuild-native.cjs && npm test && npm run bundle",
72
72
  "prepublish-check": "node scripts/pre-publish-check.cjs",
73
+ "release:patch": "node scripts/release-version.cjs patch",
74
+ "release:minor": "node scripts/release-version.cjs minor",
75
+ "release:major": "node scripts/release-version.cjs major",
76
+ "health:check": "node scripts/prod-health-check.cjs --skip-perf",
77
+ "ci:container": "bash scripts/ci-container.sh all",
78
+ "ci:container:quick": "bash scripts/ci-container.sh quick 20",
79
+ "dist:stress": "bash scripts/docker-distributed/run-distributed.sh 30",
80
+ "dist:partition": "bash scripts/docker-distributed/run-partition.sh 8",
81
+ "npm:downloads": "node scripts/record-npm-downloads.mjs",
73
82
  "prebuild-native": "node scripts/prebuild-native.cjs",
74
83
  "typecheck": "tsc --noEmit --project tsconfig.json",
75
84
  "test:ts": "tsc --noEmit --project tsconfig.json",
@@ -10,6 +10,8 @@
10
10
  * - 模拟模式 (用于测试)
11
11
  */
12
12
 
13
+ import { SSRFGuard } from '../security/ssrf-guard.js';
14
+
13
15
  export class AIService {
14
16
  constructor(options = {}) {
15
17
  this.provider = options.provider || 'mock';
@@ -183,55 +185,33 @@ class BaseClient {
183
185
 
184
186
  /**
185
187
  * 轻量级 SSRF URL 校验(用于 AI Service 内部)
186
- * 完整防护见 src/security/ssrf-guard.js
187
- * @param {string} urlStr
188
- * @param {boolean} allowLoopback
188
+ *
189
+ * 2026-10-31 渗透测试 M-05 修复:改用完整 SSRFGuard(IPv4 编码/私有网段/IPv6/协议/端口),
190
+ * 原实现只匹配点分 IPv4,`http://[::1]:8080/`、`::ffff:127.0.0.1`、十六进制/八进制 IP 变体均可绕过。
191
+ * 为兼容本地 LLM(Ollama 等自定义端口),将目标 URL 自身端口并入 allowedPorts。
192
+ * 注意:重定向跟随仍由 fetch 默认行为决定,如需逐跳复验应改用 SSRFGuard.safeFetch。
189
193
  */
190
194
  function _validateSSRFUrl(urlStr, allowLoopback) {
191
- if (typeof urlStr !== 'string' || urlStr.length === 0) {
192
- return { allowed: false, reason: 'URL 不能为空' };
193
- }
194
- let parsed;
195
195
  try {
196
- parsed = new URL(urlStr);
196
+ const parsed = new URL(urlStr);
197
+ const hostname = parsed.hostname.toLowerCase();
198
+ // SSRFGuard 静态校验只拦截 IP 字面量;`localhost` 域名需在此显式处理(保持原语义)
199
+ if (hostname === 'localhost' || hostname.endsWith('.localhost')) {
200
+ if (allowLoopback !== true) {
201
+ return { allowed: false, reason: 'localhost 被拒绝(未开启 allowLoopback)' };
202
+ }
203
+ }
204
+ const port = parsed.port
205
+ ? Number.parseInt(parsed.port, 10)
206
+ : (parsed.protocol.toLowerCase() === 'https:' ? 443 : 80);
207
+ const guard = new SSRFGuard({
208
+ allowLoopback: allowLoopback === true,
209
+ allowedPorts: [80, 443, 8080, 8443, 3000, 5000, port],
210
+ });
211
+ return guard.validate(urlStr);
197
212
  } catch (e) {
198
213
  return { allowed: false, reason: `URL 解析失败:${e.message}` };
199
214
  }
200
- const protocol = parsed.protocol.toLowerCase();
201
- if (protocol !== 'http:' && protocol !== 'https:') {
202
- return { allowed: false, reason: `协议 ${protocol} 不允许` };
203
- }
204
- const hostname = parsed.hostname.toLowerCase().replace(/^\[|\]$/g, '');
205
-
206
- // localhost 特殊处理
207
- if (hostname === 'localhost') {
208
- return allowLoopback
209
- ? { allowed: true }
210
- : { allowed: false, reason: 'localhost 被拒绝(未开启 allowLoopback)' };
211
- }
212
-
213
- // IPv4 检查
214
- const ipMatch = hostname.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
215
- if (ipMatch) {
216
- const octets = [Number(ipMatch[1]), Number(ipMatch[2]), Number(ipMatch[3]), Number(ipMatch[4])];
217
- if (octets.some(n => n > 255)) {
218
- return { allowed: false, reason: `IP ${hostname} 非法` };
219
- }
220
- const isLoopback = octets[0] === 127;
221
- const isPrivate = octets[0] === 10 ||
222
- (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) ||
223
- (octets[0] === 192 && octets[1] === 168);
224
- const isLinkLocal = octets[0] === 169 && octets[1] === 254;
225
- const isReserved = octets[0] === 0;
226
-
227
- if (isLoopback && !allowLoopback) {
228
- return { allowed: false, reason: `回环 IP ${hostname} 被拒绝` };
229
- }
230
- if (isPrivate || isLinkLocal || isReserved) {
231
- return { allowed: false, reason: `私有/链路本地/保留 IP ${hostname} 被拒绝` };
232
- }
233
- }
234
- return { allowed: true };
235
215
  }
236
216
 
237
217
  class OpenAIClient extends BaseClient {
@@ -191,6 +191,12 @@ export class WebhookChannel extends NotificationChannel {
191
191
  this.timeoutMs = options.timeoutMs || 5000;
192
192
  this.events = options.events || ['triggered', 'acknowledged', 'resolved'];
193
193
  this._ssrfGuard = options.ssrfGuard || null;
194
+ // 2026-10-31 渗透测试 R6 修复:DNS 钉扎。
195
+ // 2026-11-02 评估收尾:Webhook 是面向外部的 SSRF 暴露面(URL 来自告警通道配置),
196
+ // 钉扎默认开启(pinDns: false 可显式关闭)——在发起请求前解析并固定目标 IP,
197
+ // 消除 DNS rebinding TOCTOU 窗口;集群内部传输(raft/replication)因 Docker
198
+ // 多 A 记录/动态网络环境仍保持 opt-in。
199
+ this._pinDns = options.pinDns !== false;
194
200
  }
195
201
 
196
202
  /**
@@ -216,10 +222,10 @@ export class WebhookChannel extends NotificationChannel {
216
222
  return;
217
223
  }
218
224
 
219
- // SSRF 防护:校验 Webhook URL
225
+ // SSRF 防护:校验 Webhook URL(2026-10-31 M-04:升级为静态+DNS 深度校验)
220
226
  const guard = await this._getSSRFGuard();
221
227
  if (guard) {
222
- const check = guard.validate(this.url);
228
+ const check = await guard.validateDeep(this.url);
223
229
  if (!check.allowed) {
224
230
  this._failedCount++;
225
231
  console.warn(`[WebhookChannel] SSRF 阻止 Webhook 请求到 ${this.url}: ${check.reason}`);
@@ -227,6 +233,19 @@ export class WebhookChannel extends NotificationChannel {
227
233
  }
228
234
  }
229
235
 
236
+ // 2026-10-31 渗透测试 R6 修复:DNS 钉扎(可选,pinDns=true 时启用)
237
+ // 在发起请求前解析并固定目标 IP,消除「DNS 校验与实际请求之间」的 rebinding TOCTOU。
238
+ let pinned = null;
239
+ if (this._pinDns && guard) {
240
+ try {
241
+ pinned = await guard.resolvePinnedHost(this.url);
242
+ } catch (err) {
243
+ this._failedCount++;
244
+ console.warn(`[WebhookChannel] SSRF 钉扎失败 ${this.url}: ${err.message}`);
245
+ return;
246
+ }
247
+ }
248
+
230
249
  // 延迟加载 node:http,避免浏览器环境报错
231
250
  const http = await import('node:http');
232
251
  const https = await import('node:https');
@@ -250,15 +269,28 @@ export class WebhookChannel extends NotificationChannel {
250
269
  });
251
270
 
252
271
  const urlObj = new URL(this.url);
253
- const req = client.request(urlObj, {
272
+ const reqOptions = {
254
273
  method: 'POST',
255
274
  headers: {
256
275
  'Content-Type': 'application/json',
257
276
  'Content-Length': Buffer.byteLength(payload),
258
277
  'User-Agent': 'pure-js-db-alert-manager/1.0',
278
+ // DNS 钉扎时保留原始 Host 头(虚拟主机路由)
279
+ ...(pinned ? { Host: pinned.hostHeader } : {}),
259
280
  },
260
281
  timeout: this.timeoutMs,
261
- }, (res) => {
282
+ };
283
+ if (pinned) {
284
+ // 用已校验的解析 IP 直连;https 设置 servername 维持 SNI 与证书校验
285
+ reqOptions.hostname = pinned.ip;
286
+ reqOptions.port = pinned.port;
287
+ reqOptions.path = urlObj.pathname + urlObj.search;
288
+ if (urlObj.protocol === 'https:') {
289
+ reqOptions.servername = pinned.hostname;
290
+ }
291
+ }
292
+
293
+ const req = client.request(pinned ? reqOptions : urlObj, reqOptions, (res) => {
262
294
  res.resume(); // 消费响应体
263
295
  if (res.statusCode >= 200 && res.statusCode < 300) {
264
296
  this._sentCount++;
@@ -231,8 +231,14 @@ export class AuditLog {
231
231
  return;
232
232
  }
233
233
 
234
+ // 2026-10-31 渗透测试 L-01 修复:seq 必须单调递增。
235
+ // 原公式 _fileRecordCount + size + 1 在持久化模式下同时累加「文件总记录数」与
236
+ // 「环形缓冲当前大小」(两者同步增长),导致 seq 每次 +2(1,3,5,...),
237
+ // 且从文件恢复 N 条后续号为 2N+1,序号语义混乱(黑盒实测)。
238
+ // 持久化模式以 _fileRecordCount(含恢复记录)为准;纯内存模式无文件,
239
+ // _fileRecordCount 恒为 0,以 size 为准。
234
240
  const record = {
235
- seq: this._fileRecordCount + this.size + 1,
241
+ seq: (this._filePath ? this._fileRecordCount : this.size) + 1,
236
242
  timestamp: Date.now(),
237
243
  sessionId: this._sessionId,
238
244
  userId: entry.userId || null,
@@ -666,6 +672,46 @@ export class AuditLog {
666
672
  brokenAt = allRecords.length;
667
673
  }
668
674
  }
675
+
676
+ // P0-9 修复(2026-10-31 渗透测试 M-02):链外追加检测
677
+ // anchor 每 _anchorUpdateInterval 条更新一次,文件可能合法地比 anchor 多出若干条
678
+ // (写入后 anchor 尚未刷新)。这些"额外"行必须是哈希链的合法延续——
679
+ // 从 anchor 预期最后哈希继续向后逐条校验 prevHash 与自身 hash。
680
+ // 攻击者在文件末尾追加伪造记录(未正确续链)时在此暴露。
681
+ // 轮转延续文件(首行跨文件链引用)跳过本检查,避免旋转场景误报。
682
+ if (fileCount > this._anchorExpectedCount && !this._isContinuedChain()) {
683
+ try {
684
+ const content = fs.readFileSync(this._filePath, 'utf8');
685
+ const lines = content.split('\n').filter(l => l.trim());
686
+ let prevHashExpected = this._anchorExpectedLastHash;
687
+ let appendOk = true;
688
+ for (let i = this._anchorExpectedCount; i < lines.length; i++) {
689
+ let rec;
690
+ try {
691
+ rec = JSON.parse(lines[i]);
692
+ } catch {
693
+ appendOk = false;
694
+ break;
695
+ }
696
+ if (rec.prevHash !== prevHashExpected || rec.hash !== this._computeHash(rec)) {
697
+ appendOk = false;
698
+ break;
699
+ }
700
+ prevHashExpected = rec.hash;
701
+ }
702
+ if (!appendOk) {
703
+ errors.push(
704
+ `P0-9 检测到链外追加/篡改: anchor 预期 ${this._anchorExpectedCount} 条,文件实际 ${fileCount} 条,` +
705
+ `额外 ${fileCount - this._anchorExpectedCount} 条记录未通过哈希链延续校验`,
706
+ );
707
+ if (brokenAt === null) {
708
+ brokenAt = allRecords.length;
709
+ }
710
+ }
711
+ } catch (err) {
712
+ errors.push(`P0-9 读取审计文件失败: ${err.message}`);
713
+ }
714
+ }
669
715
  }
670
716
 
671
717
  return {
@@ -1074,10 +1120,15 @@ export class AuditLog {
1074
1120
  }
1075
1121
  if (!fileExists) {
1076
1122
  const anchorResult = await this._readAnchor();
1123
+ const errors = [];
1077
1124
  if (anchorResult.error) {
1078
1125
  console.error(`[AuditLog] recoverFromFile: ${anchorResult.error}`);
1126
+ errors.push(anchorResult.error);
1079
1127
  }
1080
- return { recovered: 0, errors: [] };
1128
+ // P0-9 修复(2026-10-31 渗透测试 M-02):双文件均不存在时给出显式告警。
1129
+ // 首次启动属正常;但若此前已写入过审计数据,则历史可能已被静默重置(删除 audit.log + .anchor)。
1130
+ errors.push('审计文件与 anchor 文件均不存在:若这不是首次启动,审计历史可能已被删除');
1131
+ return { recovered: 0, errors };
1081
1132
  }
1082
1133
 
1083
1134
  const errors = [];
@@ -31,6 +31,7 @@
31
31
  */
32
32
 
33
33
  import crypto from 'node:crypto';
34
+ import { resolve, sep } from 'node:path';
34
35
  import { VERSION } from '../version.js';
35
36
  import { Database } from '../executor/database.js';
36
37
 
@@ -65,6 +66,34 @@ export class BackupManager {
65
66
  .digest('hex');
66
67
  }
67
68
 
69
+ /**
70
+ * 解析并校验备份文件路径(2026-10-31 渗透测试 M-01 修复)
71
+ *
72
+ * 背景:MCP backup_database / restore_database 工具把调用方提供的 filePath
73
+ * 直接透传给 fs.writeFileSync / fs.readFileSync,构成任意路径写/读面。
74
+ * 现在若设置了 BACKUP_DIR(或 MCP_BACKUP_DIR)环境变量,则强制备份/恢复文件
75
+ * 必须落在该目录内(与 FDW_BASE_DIR 的路径白名单语义一致)。
76
+ *
77
+ * @param {string} filePath - 调用方提供的路径(绝对或相对)
78
+ * @returns {string} 解析后的绝对路径
79
+ * @throws {Error} 越权路径抛错
80
+ * @private
81
+ */
82
+ _resolveBackupPath(filePath) {
83
+ const resolved = resolve(String(filePath));
84
+ const backupDir = process.env.BACKUP_DIR || process.env.MCP_BACKUP_DIR;
85
+ if (backupDir) {
86
+ const baseDir = resolve(backupDir);
87
+ if (resolved !== baseDir && !resolved.startsWith(baseDir + sep)) {
88
+ throw new Error(
89
+ `备份路径越权: ${filePath} 不在 BACKUP_DIR (${baseDir}) 内。` +
90
+ '请将备份路径限制在 BACKUP_DIR/MCP_BACKUP_DIR 配置的目录内。',
91
+ );
92
+ }
93
+ }
94
+ return resolved;
95
+ }
96
+
68
97
  /**
69
98
  * P0-7 修复:验证备份文件的 HMAC
70
99
  * @param {string} payload - 备份内容 JSON 字符串(不含 hmac 字段)
@@ -134,6 +163,7 @@ export class BackupManager {
134
163
  if (!db) {
135
164
  throw new Error('fullBackup 需要传入 db 参数');
136
165
  }
166
+ const resolvedPath = this._resolveBackupPath(filePath);
137
167
  const data = this._wal.serializeDatabase(db);
138
168
  const lastSeq = await this._wal._currentSeq();
139
169
  const backup = {
@@ -149,10 +179,10 @@ export class BackupManager {
149
179
  if (hmac) {
150
180
  backup.hmac = hmac;
151
181
  }
152
- fs.writeFileSync(filePath, JSON.stringify(backup, null, 2));
182
+ fs.writeFileSync(resolvedPath, JSON.stringify(backup, null, 2));
153
183
  this._backupHistory.push({
154
184
  type: 'full',
155
- filePath,
185
+ filePath: resolvedPath,
156
186
  timestamp: backup.timestamp,
157
187
  lastSeq: backup.lastSeq,
158
188
  });
@@ -170,6 +200,7 @@ export class BackupManager {
170
200
  */
171
201
  async incrementalBackup(filePath, fromSeq) {
172
202
  const fs = await import('fs');
203
+ const resolvedPath = this._resolveBackupPath(filePath);
173
204
  const entries = await this._wal.getEntriesAfter(fromSeq);
174
205
  const toSeq = await this._wal._currentSeq();
175
206
  const backup = {
@@ -186,10 +217,10 @@ export class BackupManager {
186
217
  if (hmac) {
187
218
  backup.hmac = hmac;
188
219
  }
189
- fs.writeFileSync(filePath, JSON.stringify(backup, null, 2));
220
+ fs.writeFileSync(resolvedPath, JSON.stringify(backup, null, 2));
190
221
  this._backupHistory.push({
191
222
  type: 'incremental',
192
- filePath,
223
+ filePath: resolvedPath,
193
224
  timestamp: backup.timestamp,
194
225
  fromSeq,
195
226
  toSeq: backup.toSeq,
@@ -207,7 +238,8 @@ export class BackupManager {
207
238
  */
208
239
  async restore(filePath) {
209
240
  const fs = await import('fs');
210
- const content = fs.readFileSync(filePath, 'utf-8');
241
+ const resolvedPath = this._resolveBackupPath(filePath);
242
+ const content = fs.readFileSync(resolvedPath, 'utf-8');
211
243
  const backup = JSON.parse(content);
212
244
 
213
245
  // P0-7 修复:验证 HMAC
@@ -237,7 +269,8 @@ export class BackupManager {
237
269
  */
238
270
  async pointInTimeRestore(baseBackupPath, walDirectory, targetTime) {
239
271
  const fs = await import('fs');
240
- const content = fs.readFileSync(baseBackupPath, 'utf-8');
272
+ const resolvedBasePath = this._resolveBackupPath(baseBackupPath);
273
+ const content = fs.readFileSync(resolvedBasePath, 'utf-8');
241
274
  const baseBackup = JSON.parse(content);
242
275
 
243
276
  // P0-7 修复:验证 baseBackup 的 HMAC
@@ -45,6 +45,8 @@ export class RaftHttpTransport {
45
45
  allowedPorts: [],
46
46
  };
47
47
  this._ssrfGuard = null;
48
+ // 2026-10-31 渗透测试 R6 修复:DNS 钉扎(可选,默认关闭)
49
+ this._pinDns = options.pinDns === true;
48
50
 
49
51
  /** @type {RaftNode|null} */
50
52
  this._node = null;
@@ -141,10 +143,10 @@ export class RaftHttpTransport {
141
143
 
142
144
  const fullUrl = `http://${address}${path}`;
143
145
 
144
- // v1.34.5: SSRF 防护校验目标节点地址
146
+ // v1.34.5: SSRF 防护校验目标节点地址(2026-10-31 M-04:升级为静态+DNS 深度校验)
145
147
  const guard = await this._getSSRFGuard();
146
148
  if (guard) {
147
- const check = guard.validate(fullUrl);
149
+ const check = await guard.validateDeep(fullUrl);
148
150
  if (!check.allowed) {
149
151
  throw new Error(`SSRF 阻止 Raft RPC 到 ${targetId} (${fullUrl}): ${check.reason}`);
150
152
  }
@@ -152,14 +154,22 @@ export class RaftHttpTransport {
152
154
 
153
155
  const body = JSON.stringify(request);
154
156
  const url = new URL(fullUrl);
157
+
158
+ // 2026-10-31 渗透测试 R6 修复:DNS 钉扎(可选)——用解析 IP 直连,保留 Host 头
159
+ let pinned = null;
160
+ if (this._pinDns && guard) {
161
+ pinned = await guard.resolvePinnedHost(fullUrl);
162
+ }
163
+
155
164
  const reqOpts = {
156
- hostname: url.hostname,
157
- port: url.port,
165
+ hostname: pinned ? pinned.ip : url.hostname,
166
+ port: pinned ? pinned.port : url.port,
158
167
  path: url.pathname,
159
168
  method: 'POST',
160
169
  headers: {
161
170
  'Content-Type': 'application/json',
162
171
  'Content-Length': Buffer.byteLength(body),
172
+ ...(pinned ? { Host: pinned.hostHeader } : {}),
163
173
  },
164
174
  timeout: this.timeout,
165
175
  };
@@ -9731,7 +9731,22 @@ export class Executor {
9731
9731
  */
9732
9732
  enableAuditLog(options = {}) {
9733
9733
  this._auditLog = new AuditLog(options);
9734
- this._auditLog.enable();
9734
+ // 2026-10-31 渗透测试 M-02 修复:将恢复结果(含 anchor 缺失/历史可能被删除等告警)
9735
+ // 通过 console.warn 暴露给运维;原实现静默吞掉 errors,导致删除审计文件不被察觉。
9736
+ const enableResult = this._auditLog.enable();
9737
+ if (enableResult && typeof enableResult.then === 'function') {
9738
+ enableResult
9739
+ .then((result) => {
9740
+ const errs = result?.errors || [];
9741
+ if (errs.length > 0) {
9742
+ console.warn(`[AuditLog] 恢复警告(${errs.length} 项):`);
9743
+ for (const e2 of errs) console.warn(` - ${e2}`);
9744
+ }
9745
+ })
9746
+ .catch((err) => {
9747
+ console.warn(`[AuditLog] 启用审计失败: ${err?.message || err}`);
9748
+ });
9749
+ }
9735
9750
  return this._auditLog;
9736
9751
  }
9737
9752
 
@@ -10386,6 +10401,14 @@ export class Executor {
10386
10401
  }
10387
10402
  }
10388
10403
  const result = this.executeSQL(sql, params, options);
10404
+ // 2026-10-31 渗透测试 C-01 修复:并行 SELECT 路径(autoParallel + WorkerPool)使
10405
+ // executeSQL 返回 Promise.all(...)。若此 Promise 在下方 setImmediate 让出期间拒绝,
10406
+ // 且尚未挂接任何 handler,Node 会触发 unhandledRejection 默认致命错误,直接崩溃进程。
10407
+ // 这里在让出前立即挂接一个 no-op catch(标记为已处理,不影响 return 采纳原始状态),
10408
+ // 避免服务被任意 SELECT 运行时错误(缺表/缺列/类型错误)远程打崩。
10409
+ if (result && typeof result.then === 'function') {
10410
+ result.catch(() => { /* 仅标记已处理,错误仍由调用方 await 观察到 */ });
10411
+ }
10389
10412
  // 执行后让出:结果返回前事件循环有机会处理新到达的请求
10390
10413
  await new Promise((resolve) => setImmediate(resolve));
10391
10414
  return result;