MCPcopy Create free account
hub / github.com/chhoumann/quickadd / assertWriteStaysInVault

Function assertWriteStaysInVault

src/utils/vaultWriteGuards.ts:50–114  ·  view source on GitHub ↗
(
	app: App,
	vaultRelativePath: string,
)

Source from the content-addressed store, hash-verified

48}
49
50export async function assertWriteStaysInVault(
51 app: App,
52 vaultRelativePath: string,
53): Promise<void> {
54 const adapter = app.vault.adapter;
55 // `typeof` guard: FileSystemAdapter is absent on mobile and in the test stub —
56 // `instanceof undefined` would throw, so check the symbol is a constructor first.
57 if (typeof FileSystemAdapter !== "function" || !(adapter instanceof FileSystemAdapter)) {
58 return; // mobile / no real FS — string sanitization is the only guard there
59 }
60
61 const fs = nodeRequire<NodeFs>("fs");
62 const path = nodeRequire<NodePath>("path");
63 if (!fs?.promises || !path) return; // can't verify — fail open only when fs is unavailable
64
65 const realBase = await fs.promises.realpath(adapter.getBasePath());
66 const targetFull = adapter.getFullPath(vaultRelativePath);
67
68 // Walk up to the nearest path that actually exists; resolve ITS realpath. Any
69 // symlink in the existing chain (pointing outside the vault) surfaces here. The
70 // not-yet-created tail is plain, already-sanitized names, so it cannot escape.
71 let probe = targetFull;
72 while (!exists(fs, probe)) {
73 const parent = path.dirname(probe);
74 if (parent === probe) break;
75 probe = parent;
76 }
77
78 let realTarget: string;
79 try {
80 realTarget = await fs.promises.realpath(probe);
81 } catch {
82 // `probe` exists (lstatSync above) yet cannot be resolved: it is, or passes
83 // through, a DANGLING symlink whose ultimate target does not exist. A write
84 // still FOLLOWS that symlink and can land outside the vault, and we cannot
85 // prove where — so fail CLOSED. (On Linux/glibc realpath throws ENOENT here;
86 // the previous `.catch(() => probe)` fallback trusted the unresolved in-vault
87 // path and let the escape through.)
88 throw new VaultWriteEscapeError(
89 `Refusing to write to "${vaultRelativePath}": it resolves through an unresolvable (dangling) symlink.`,
90 );
91 }
92 const rel = path.relative(realBase, realTarget);
93 const escapes = rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel);
94 if (escapes) {
95 throw new VaultWriteEscapeError(
96 `Refusing to write to "${vaultRelativePath}": it resolves (via a symlink) outside the vault.`,
97 );
98 }
99
100 // Reject a target that RESOLVES into a dot/config directory (.obsidian, .git,
101 // .trash, ...) even though it stays inside the vault. The lexical per-segment
102 // dot-floor only sees the LITERAL destination, so a pre-existing in-vault
103 // symlink like `safe -> .obsidian/plugins/pkg` would otherwise smuggle an
104 // untrusted "safe/main.js" write into a config/executable directory. Checking
105 // the realpath-resolved segments closes that symlink-mediated config-dir drop.
106 const resolvedEscapesIntoConfigDir = rel
107 .split(path.sep)

Callers 3

createVaultToolsFunction · 0.90
applyPackageImportFunction · 0.90

Calls 7

getBasePathMethod · 0.80
getFullPathMethod · 0.80
dirnameMethod · 0.80
relativeMethod · 0.80
isAbsoluteMethod · 0.80
nodeRequireFunction · 0.70
existsFunction · 0.70

Tested by

no test coverage detected