diff --git a/src/permission/path-restriction.test.ts b/src/permission/path-restriction.test.ts index 8d677dab5..53811091d 100644 --- a/src/permission/path-restriction.test.ts +++ b/src/permission/path-restriction.test.ts @@ -48,6 +48,22 @@ test("workspace-relative paths are unrestricted", () => { expect(r.isRestricted("src/index.ts", true)).toBe(false); }); +test("an empty-string root does not turn containment into allow-all", () => { + // Regression for CL-6700: root + sep === sep when root is "", which every + // absolute path starts with. A sensitive absolute path resolved against + // a provider that yields "" roots must still be denied. + const r = createPathRestriction(cwd, () => [""], home); + expect(r.isRestricted("/etc/passwd", false)).toBe(true); + expect(r.isRestricted("/etc/passwd", true)).toBe(true); +}); + +test("a root of exactly \"/\" is not the same bug: it is not an allow-all prefix", () => { + // Documents the non-bug: "/" + sep is "//", which "/etc/passwd" does not + // start with, so an unrelated absolute path stays outside the workspace. + const r = createPathRestriction(cwd, () => ["/"], home); + expect(r.isRestricted("/etc/passwd", false)).toBe(true); +}); + test("a directory sharing a string prefix with the workspace root is still restricted", async () => { // "baz" shares a string prefix with cwd but is a distinct sibling // directory outside the workspace — the boundary check must not leak into diff --git a/src/permission/path-restriction.ts b/src/permission/path-restriction.ts index 6950df9a6..9f24bd2b4 100644 --- a/src/permission/path-restriction.ts +++ b/src/permission/path-restriction.ts @@ -58,8 +58,12 @@ function realpathNearestOr(path: string): string { } } +// An empty root must never reach the prefix compare: `"" + sep` is just +// `sep` (e.g. "/" on Unix), which every absolute path starts with, turning +// containment into allow-all. A root of exactly `sep` itself is not this bug +// — `startsWith(sep + sep)` correctly rejects unrelated absolute paths. const inKnownRoots = (real: string, roots: readonly string[]): boolean => - roots.some((root) => real === root || real.startsWith(root + sep)); + roots.some((root) => root.length > 0 && (real === root || real.startsWith(root + sep))); // Resolves `path` (relative or absolute, possibly traversing `..`) against // `cwd` and checks it against the workspace boundary: `cwd` itself plus every