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/README.md
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
> **零依赖嵌入式数据库引擎 —— 单文件部署,同时支持向量搜索、MVCC 并发写入、CDC 变更捕获。**
|
|
4
4
|
> 如果你的 Node.js 应用需要 PostgreSQL + Milvus + Debezium 的能力但不想要三个独立服务,这里一个文件就够。
|
|
5
5
|
|
|
6
|
-
> 📌 **当前状态**:v2.53.
|
|
6
|
+
> 📌 **当前状态**:v2.53.1,成熟度 **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
|

|
|
9
9
|

|
|
10
|
-

|
|
11
11
|

|
|
12
12
|

|
|
13
13
|

|
|
@@ -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.
|
|
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.1)的性能承诺。
|
|
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.
|
|
3
|
+
"version": "2.53.1",
|
|
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",
|
package/src/ai/ai-service.js
CHANGED
|
@@ -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
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
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,9 @@ 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
|
+
// 启用后使用 resolvePinnedHost 返回的解析 IP 直连,消除 DNS rebinding TOCTOU 窗口。
|
|
196
|
+
this._pinDns = options.pinDns === true;
|
|
194
197
|
}
|
|
195
198
|
|
|
196
199
|
/**
|
|
@@ -216,10 +219,10 @@ export class WebhookChannel extends NotificationChannel {
|
|
|
216
219
|
return;
|
|
217
220
|
}
|
|
218
221
|
|
|
219
|
-
// SSRF 防护:校验 Webhook URL
|
|
222
|
+
// SSRF 防护:校验 Webhook URL(2026-10-31 M-04:升级为静态+DNS 深度校验)
|
|
220
223
|
const guard = await this._getSSRFGuard();
|
|
221
224
|
if (guard) {
|
|
222
|
-
const check = guard.
|
|
225
|
+
const check = await guard.validateDeep(this.url);
|
|
223
226
|
if (!check.allowed) {
|
|
224
227
|
this._failedCount++;
|
|
225
228
|
console.warn(`[WebhookChannel] SSRF 阻止 Webhook 请求到 ${this.url}: ${check.reason}`);
|
|
@@ -227,6 +230,19 @@ export class WebhookChannel extends NotificationChannel {
|
|
|
227
230
|
}
|
|
228
231
|
}
|
|
229
232
|
|
|
233
|
+
// 2026-10-31 渗透测试 R6 修复:DNS 钉扎(可选,pinDns=true 时启用)
|
|
234
|
+
// 在发起请求前解析并固定目标 IP,消除「DNS 校验与实际请求之间」的 rebinding TOCTOU。
|
|
235
|
+
let pinned = null;
|
|
236
|
+
if (this._pinDns && guard) {
|
|
237
|
+
try {
|
|
238
|
+
pinned = await guard.resolvePinnedHost(this.url);
|
|
239
|
+
} catch (err) {
|
|
240
|
+
this._failedCount++;
|
|
241
|
+
console.warn(`[WebhookChannel] SSRF 钉扎失败 ${this.url}: ${err.message}`);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
230
246
|
// 延迟加载 node:http,避免浏览器环境报错
|
|
231
247
|
const http = await import('node:http');
|
|
232
248
|
const https = await import('node:https');
|
|
@@ -250,15 +266,28 @@ export class WebhookChannel extends NotificationChannel {
|
|
|
250
266
|
});
|
|
251
267
|
|
|
252
268
|
const urlObj = new URL(this.url);
|
|
253
|
-
const
|
|
269
|
+
const reqOptions = {
|
|
254
270
|
method: 'POST',
|
|
255
271
|
headers: {
|
|
256
272
|
'Content-Type': 'application/json',
|
|
257
273
|
'Content-Length': Buffer.byteLength(payload),
|
|
258
274
|
'User-Agent': 'pure-js-db-alert-manager/1.0',
|
|
275
|
+
// DNS 钉扎时保留原始 Host 头(虚拟主机路由)
|
|
276
|
+
...(pinned ? { Host: pinned.hostHeader } : {}),
|
|
259
277
|
},
|
|
260
278
|
timeout: this.timeoutMs,
|
|
261
|
-
}
|
|
279
|
+
};
|
|
280
|
+
if (pinned) {
|
|
281
|
+
// 用已校验的解析 IP 直连;https 设置 servername 维持 SNI 与证书校验
|
|
282
|
+
reqOptions.hostname = pinned.ip;
|
|
283
|
+
reqOptions.port = pinned.port;
|
|
284
|
+
reqOptions.path = urlObj.pathname + urlObj.search;
|
|
285
|
+
if (urlObj.protocol === 'https:') {
|
|
286
|
+
reqOptions.servername = pinned.hostname;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const req = client.request(pinned ? reqOptions : urlObj, reqOptions, (res) => {
|
|
262
291
|
res.resume(); // 消费响应体
|
|
263
292
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
264
293
|
this._sentCount++;
|
package/src/audit/audit-log.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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.
|
|
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
|
};
|
package/src/executor/executor.js
CHANGED
|
@@ -9731,7 +9731,22 @@ export class Executor {
|
|
|
9731
9731
|
*/
|
|
9732
9732
|
enableAuditLog(options = {}) {
|
|
9733
9733
|
this._auditLog = new AuditLog(options);
|
|
9734
|
-
|
|
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;
|