Skip to content

Commit db52cc5

Browse files
committed
fix: keep lexical containment in open service allowed-root check
1 parent 5348f39 commit db52cc5

1 file changed

Lines changed: 14 additions & 15 deletions

File tree

services/open/src/index.ts

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -94,27 +94,26 @@ export function createOpenService(options?: OpenServiceOptions): DevframeService
9494
// Option sets from multiple installers merge via devframe's default
9595
// deep-merge: `roots` union, `editor` last-wins.
9696
setup(ctx, { options }) {
97-
// Canonicalize each allowed root once (resolving symlinks in the root
98-
// paths themselves) so containment compares canonical to canonical.
99-
const allowedRoots = Promise.all(
100-
[ctx.workspaceRoot, ...(options?.roots ?? [])].map(r => nearestExistingCanonical(resolve(r))),
101-
)
97+
const allowedRoots = [ctx.workspaceRoot, ...(options?.roots ?? [])].map(r => resolve(r))
98+
// Canonical forms of the same roots (symlinks in the root paths
99+
// resolved), computed once for the symlink-aware pass.
100+
const canonicalRoots = Promise.all(allowedRoots.map(r => nearestExistingCanonical(r)))
101+
102+
const within = (roots: string[], p: string): boolean => roots.some((root) => {
103+
const rel = relative(root, p)
104+
return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel))
105+
})
102106

103107
/**
104-
* Resolve `path` (relative paths against `workspaceRoot`) and assert
105-
* its canonical location lands inside one of the allowed roots, or
106-
* throw. Canonicalizing the nearest existing ancestor rejects a symlink
107-
* that would redirect the open outside every allowed root, while still
108+
* Resolve `path` (relative paths against `workspaceRoot`) and assert it
109+
* lands inside one of the allowed roots, or throw. The lexical pass
110+
* rejects plain `..`/absolute escapes; the canonical pass rejects a
111+
* symlink that would redirect the open outside every root, while still
108112
* allowing not-yet-existing files under a root.
109113
*/
110114
async function assertAllowedPath(path: string): Promise<string> {
111115
const resolved = isAbsolute(path) ? resolve(path) : resolve(ctx.workspaceRoot, path)
112-
const canonical = await nearestExistingCanonical(resolved)
113-
const contained = (await allowedRoots).some((root) => {
114-
const rel = relative(root, canonical)
115-
return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel))
116-
})
117-
if (!contained)
116+
if (!within(allowedRoots, resolved) || !within(await canonicalRoots, await nearestExistingCanonical(resolved)))
118117
throw diagnostics.DS_OPEN_0002({ path })
119118
return resolved
120119
}

0 commit comments

Comments
 (0)