backupdrill 2.1.1 → 2.1.3
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/dist/lib.d.ts +2 -0
- package/dist/lib.js +3 -0
- package/dist/restore-engine.d.ts +12 -3
- package/dist/restore-engine.js +50 -7
- package/package.json +1 -1
package/dist/lib.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export { runBackup } from "./backup.js";
|
|
|
2
2
|
export { SUPABASE_ROOT_CA, pgConnectOptions } from "./supabase-ca.js";
|
|
3
3
|
export { runDrill, drillDump } from "./drill.js";
|
|
4
4
|
export { runRestore } from "./restore.js";
|
|
5
|
+
export { projectRefOf, assertNoHostOverride } from "./restore-engine.js";
|
|
6
|
+
export { sameDatabaseTarget } from "./restore.js";
|
|
5
7
|
export { runEstimate, project } from "./estimate.js";
|
|
6
8
|
export { loadConfig } from "./config.js";
|
|
7
9
|
export { targetClient, resolveSnapshot, getObjectText, downloadToFile, hashObject, } from "./snapshots.js";
|
package/dist/lib.js
CHANGED
|
@@ -6,6 +6,9 @@ export { runBackup } from "./backup.js";
|
|
|
6
6
|
export { SUPABASE_ROOT_CA, pgConnectOptions } from "./supabase-ca.js";
|
|
7
7
|
export { runDrill, drillDump } from "./drill.js";
|
|
8
8
|
export { runRestore } from "./restore.js";
|
|
9
|
+
// 身份判定三件套对外导出:托管 worker 用它们与 app 侧 shared 的副本做同规则回归(交叉审查)
|
|
10
|
+
export { projectRefOf, assertNoHostOverride } from "./restore-engine.js";
|
|
11
|
+
export { sameDatabaseTarget } from "./restore.js";
|
|
9
12
|
export { runEstimate, project } from "./estimate.js";
|
|
10
13
|
export { loadConfig } from "./config.js";
|
|
11
14
|
export { targetClient, resolveSnapshot, getObjectText, downloadToFile, hashObject, } from "./snapshots.js";
|
package/dist/restore-engine.d.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type { ExtensionInfo } from "./manifest.js";
|
|
2
2
|
export type RestoreTargetKind = "sandbox" | "supabase";
|
|
3
|
-
/**
|
|
4
|
-
*
|
|
3
|
+
/** 主机名规范化:先按驱动语义百分号解码(pg-connection-string / libpq 都会解码主机名,
|
|
4
|
+
* `supabase%2Ecom` 在驱动眼里就是 supabase.com),再剥掉合法的 DNS 根点(db.x.supabase.co. ≡
|
|
5
|
+
* db.x.supabase.co)。身份比较不规范化 = 一个尾点或一个 %2E 就能绕过同源阻断(交叉审查)。 */
|
|
5
6
|
export declare function normalizeHost(hostname: string): string;
|
|
6
7
|
/**
|
|
7
8
|
* 从连接串提取 Supabase 项目 ref(纯函数):直连主机 `db.<ref>.supabase.co` 或
|
|
8
|
-
* pooler 用户名
|
|
9
|
+
* pooler 用户名 `<role>.<ref>`:Supavisor 在**最后一个点**切分租户(handler_helpers.ex),角色名
|
|
10
|
+
* 可以含大写、连字符甚至点,所以角色段不做限制、只认最后一段 ref;并且必须是 Supabase pooler
|
|
11
|
+
* 主机——同样形状的用户名落在别的主机上不是 Supabase 租户身份(交叉审查)。
|
|
12
|
+
* 放在引擎层:备份端(写 manifest.sourceProjectRef)
|
|
9
13
|
* 与恢复端(同源阻断)共用同一个身份判定。非 Supabase 形态返回 null。
|
|
10
14
|
*/
|
|
11
15
|
export declare function projectRefOf(connString: string): string | null;
|
|
@@ -71,6 +75,11 @@ export declare function credentialSafeDbArgs(connString: string): {
|
|
|
71
75
|
* 拒绝连接串里的 host/hostaddr/user 查询覆盖:驱动会让它们改写实际连接目标/租户——
|
|
72
76
|
* 身份判定(ref/主机)看的是 authority,读写却发生在别处。备份侧(源身份自记)与
|
|
73
77
|
* 恢复侧(确认门/同源阻断)共用本检查。
|
|
78
|
+
* Supabase pooler 主机上再拒绝**任何** `?options=`:Supavisor 从 options 里解析 `reference=<ref>`
|
|
79
|
+
* 并让它优先于用户名里的租户段(handler_helpers.ex),而且会再解一次编码、认反斜杠转义——
|
|
80
|
+
* 试图识别"哪种 options 值是租户覆盖"是在追它的解析器,`%2572eference` 这类双重编码就绕过了
|
|
81
|
+
* (交叉审查)。pooler 串本来就不需要 options,整个参数一律拒绝;非 pooler 主机(普通 Postgres
|
|
82
|
+
* 目标)的 options 没有租户语义,照常放行。
|
|
74
83
|
*/
|
|
75
84
|
export declare function assertNoHostOverride(connString: string): void;
|
|
76
85
|
export declare function restoreDatabaseArtifact(opts: {
|
package/dist/restore-engine.js
CHANGED
|
@@ -11,14 +11,25 @@
|
|
|
11
11
|
import { spawn } from "node:child_process";
|
|
12
12
|
import { resolvePgRestoreBin } from "./pgbin.js";
|
|
13
13
|
import { dumpUrlFor, connectPg } from "./supabase-ca.js";
|
|
14
|
-
/**
|
|
15
|
-
*
|
|
14
|
+
/** 主机名规范化:先按驱动语义百分号解码(pg-connection-string / libpq 都会解码主机名,
|
|
15
|
+
* `supabase%2Ecom` 在驱动眼里就是 supabase.com),再剥掉合法的 DNS 根点(db.x.supabase.co. ≡
|
|
16
|
+
* db.x.supabase.co)。身份比较不规范化 = 一个尾点或一个 %2E 就能绕过同源阻断(交叉审查)。 */
|
|
16
17
|
export function normalizeHost(hostname) {
|
|
17
|
-
|
|
18
|
+
let decoded = hostname;
|
|
19
|
+
try {
|
|
20
|
+
decoded = decodeURIComponent(hostname);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// 非法百分号序列:驱动同样解不开,按原样比较
|
|
24
|
+
}
|
|
25
|
+
return decoded.replace(/\.$/, "").toLowerCase();
|
|
18
26
|
}
|
|
19
27
|
/**
|
|
20
28
|
* 从连接串提取 Supabase 项目 ref(纯函数):直连主机 `db.<ref>.supabase.co` 或
|
|
21
|
-
* pooler 用户名
|
|
29
|
+
* pooler 用户名 `<role>.<ref>`:Supavisor 在**最后一个点**切分租户(handler_helpers.ex),角色名
|
|
30
|
+
* 可以含大写、连字符甚至点,所以角色段不做限制、只认最后一段 ref;并且必须是 Supabase pooler
|
|
31
|
+
* 主机——同样形状的用户名落在别的主机上不是 Supabase 租户身份(交叉审查)。
|
|
32
|
+
* 放在引擎层:备份端(写 manifest.sourceProjectRef)
|
|
22
33
|
* 与恢复端(同源阻断)共用同一个身份判定。非 Supabase 形态返回 null。
|
|
23
34
|
*/
|
|
24
35
|
export function projectRefOf(connString) {
|
|
@@ -29,8 +40,14 @@ export function projectRefOf(connString) {
|
|
|
29
40
|
return direct[1];
|
|
30
41
|
// 用户名必须先解码再匹配:URL 解析器保留百分号编码,而 pg/libpq 会解码——
|
|
31
42
|
// postgres%2Eref 在驱动眼里就是 postgres.ref,不解码 = 身份判定可被编码绕过
|
|
32
|
-
|
|
33
|
-
|
|
43
|
+
// [\s\S] 而不是 .:Postgres 加引号的角色名可含换行,`.` 不匹配行终止符会让这类角色身份判定失效。
|
|
44
|
+
// 解码后含 NUL 一律不认:启动包用 NUL 分隔字段,`postgres%00options%00reference=…%00x.<ref>`
|
|
45
|
+
// 在驱动/Supavisor 眼里是 user=postgres 外加一个 options 字段,身份判定看到的 ref 是假的(交叉审查)
|
|
46
|
+
const username = decodeURIComponent(url.username);
|
|
47
|
+
if (username.includes("\0"))
|
|
48
|
+
return null;
|
|
49
|
+
const pooled = username.match(/^[\s\S]+\.([a-z0-9]{16,})$/);
|
|
50
|
+
if (pooled && /\.pooler\.supabase\.com$/.test(normalizeHost(url.hostname)))
|
|
34
51
|
return pooled[1];
|
|
35
52
|
return null;
|
|
36
53
|
}
|
|
@@ -233,15 +250,41 @@ export function credentialSafeDbArgs(connString) {
|
|
|
233
250
|
* 拒绝连接串里的 host/hostaddr/user 查询覆盖:驱动会让它们改写实际连接目标/租户——
|
|
234
251
|
* 身份判定(ref/主机)看的是 authority,读写却发生在别处。备份侧(源身份自记)与
|
|
235
252
|
* 恢复侧(确认门/同源阻断)共用本检查。
|
|
253
|
+
* Supabase pooler 主机上再拒绝**任何** `?options=`:Supavisor 从 options 里解析 `reference=<ref>`
|
|
254
|
+
* 并让它优先于用户名里的租户段(handler_helpers.ex),而且会再解一次编码、认反斜杠转义——
|
|
255
|
+
* 试图识别"哪种 options 值是租户覆盖"是在追它的解析器,`%2572eference` 这类双重编码就绕过了
|
|
256
|
+
* (交叉审查)。pooler 串本来就不需要 options,整个参数一律拒绝;非 pooler 主机(普通 Postgres
|
|
257
|
+
* 目标)的 options 没有租户语义,照常放行。
|
|
236
258
|
*/
|
|
237
259
|
export function assertNoHostOverride(connString) {
|
|
238
260
|
try {
|
|
239
|
-
const
|
|
261
|
+
const url = new URL(connString);
|
|
262
|
+
// 用户名/密码/主机里解码后出现 NUL:启动包以 NUL 分隔字段,后面的内容会被当成额外的启动参数
|
|
263
|
+
// (options=reference=… 之类),连接目标与身份判定脱节。libpq 稍后也会拒,但驱动侧先发出的
|
|
264
|
+
// 启动包已经到了 pooler——在任何连接/身份判定之前就拒(交叉审查)。
|
|
265
|
+
for (const [label, raw] of [["username", url.username], ["password", url.password], ["host", url.hostname]]) {
|
|
266
|
+
let decoded = raw;
|
|
267
|
+
try {
|
|
268
|
+
decoded = decodeURIComponent(raw);
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
// 非法编码:驱动同样解不开,按原样看
|
|
272
|
+
}
|
|
273
|
+
if (decoded.includes("\0")) {
|
|
274
|
+
throw new Error(`connection string ${label} contains a NUL byte override — use a plain connection string.`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const params = url.searchParams;
|
|
278
|
+
const isSupabasePooler = /\.pooler\.supabase\.com$/.test(normalizeHost(url.hostname));
|
|
240
279
|
for (const key of params.keys()) {
|
|
241
280
|
if (/^(host|hostaddr|user)$/i.test(key)) {
|
|
242
281
|
throw new Error(`connection string carries a ?${key}= override — the effective server/identity would ` +
|
|
243
282
|
`differ from the URL authority that identity checks inspect. Use a plain connection string.`);
|
|
244
283
|
}
|
|
284
|
+
if (isSupabasePooler && /^options$/i.test(key)) {
|
|
285
|
+
throw new Error("connection string carries ?options= on a Supabase pooler host — the pooler reads a tenant override " +
|
|
286
|
+
"(reference=…) from it, so identity checks could not trust the username. Remove ?options= entirely.");
|
|
287
|
+
}
|
|
245
288
|
}
|
|
246
289
|
}
|
|
247
290
|
catch (error) {
|
package/package.json
CHANGED