fix(root): align pinned fallback path boundary checks

This commit is contained in:
Peter Steinberger 2026-05-08 02:43:24 +01:00
parent ecefc5bd53
commit 36dd0888a3
No known key found for this signature in database
5 changed files with 45 additions and 12 deletions

View File

@ -8,7 +8,7 @@ const LINE_BUDGETS = new Map([
["src/pinned-python.ts", 655],
["src/root-impl.ts", 1750],
["src/root-path.ts", 862],
["test/api-coverage.test.ts", 982],
["test/api-coverage.test.ts", 983],
["test/new-primitives.test.ts", 1500],
]);

View File

@ -1334,10 +1334,11 @@ async function resolvePinnedOperationPathInRoot(
if ((relativeResolved === "" || relativeResolved === ".") && params.allowRoot === true) {
return { rootReal: resolved.rootReal, resolved: resolved.canonicalPath, relativePosix: "" };
}
const firstSegment = relativeResolved.split(path.sep)[0];
if (
relativeResolved === "" ||
relativeResolved === "." ||
relativeResolved.startsWith("..") ||
firstSegment === ".." ||
path.isAbsolute(relativeResolved)
) {
throw new FsSafeError("outside-workspace", "file is outside workspace root");

View File

@ -100,10 +100,15 @@ export async function makeTempLayout(
return { outside, outsideFile, root };
}
export function expectFsSafeCode(error: unknown, codes: readonly string[]): void {
export function expectFsSafeCode(
error: unknown,
codes: readonly string[],
opts: { allowUnsupportedPlatformOnWindows?: boolean } = {},
): void {
expect(error).toBeInstanceOf(FsSafeError);
const accepted =
process.platform === "win32" ? [...codes, "unsupported-platform"] : codes;
const accepted = process.platform === "win32" && opts.allowUnsupportedPlatformOnWindows
? [...codes, "unsupported-platform"]
: codes;
expect(accepted).toContain((error as FsSafeError).code);
}

View File

@ -72,11 +72,15 @@ describe("read boundary bypass attempts", () => {
return true;
});
await expect(safeRoot.stat("../secret.txt")).rejects.toSatisfy((error: unknown) => {
expectFsSafeCode(error, ["outside-workspace", "invalid-path", "path-alias"]);
expectFsSafeCode(error, ["outside-workspace", "invalid-path", "path-alias"], {
allowUnsupportedPlatformOnWindows: true,
});
return true;
});
await expect(safeRoot.list(".." as string)).rejects.toSatisfy((error: unknown) => {
expectFsSafeCode(error, ["outside-workspace", "invalid-path", "path-alias"]);
expectFsSafeCode(error, ["outside-workspace", "invalid-path", "path-alias"], {
allowUnsupportedPlatformOnWindows: true,
});
return true;
});
await expect(scope.files(["../secret.txt"])).resolves.toMatchObject({ ok: false });
@ -96,11 +100,15 @@ describe("read boundary bypass attempts", () => {
return true;
});
await expect(safeRoot.stat("link/secret.txt")).rejects.toSatisfy((error: unknown) => {
expectFsSafeCode(error, ["outside-workspace", "path-alias", "symlink"]);
expectFsSafeCode(error, ["outside-workspace", "path-alias", "symlink"], {
allowUnsupportedPlatformOnWindows: true,
});
return true;
});
await expect(safeRoot.list("link")).rejects.toSatisfy((error: unknown) => {
expectFsSafeCode(error, ["outside-workspace", "path-alias", "symlink"]);
expectFsSafeCode(error, ["outside-workspace", "path-alias", "symlink"], {
allowUnsupportedPlatformOnWindows: true,
});
return true;
});
});
@ -120,7 +128,9 @@ describe("read boundary bypass attempts", () => {
return true;
});
await expect(safeRoot.stat("secret-link.txt")).rejects.toSatisfy((error: unknown) => {
expectFsSafeCode(error, ["outside-workspace", "path-alias", "symlink"]);
expectFsSafeCode(error, ["outside-workspace", "path-alias", "symlink"], {
allowUnsupportedPlatformOnWindows: true,
});
return true;
});
@ -168,7 +178,9 @@ describe("read boundary bypass attempts", () => {
return true;
});
await expect(safeRoot.stat(layout.outsideFile)).rejects.toSatisfy((error: unknown) => {
expectFsSafeCode(error, ["outside-workspace", "path-alias", "invalid-path"]);
expectFsSafeCode(error, ["outside-workspace", "path-alias", "invalid-path"], {
allowUnsupportedPlatformOnWindows: true,
});
return true;
});

View File

@ -3,7 +3,7 @@ import path from "node:path";
import { Readable } from "node:stream";
import { afterEach, describe, expect, it } from "vitest";
import { fileStore, fileStoreSync } from "../src/file-store.js";
import { root as openRoot } from "../src/index.js";
import { configureFsSafePython, root as openRoot } from "../src/index.js";
import {
ESCAPING_DIRECTORY_PAYLOADS,
ESCAPING_WRITE_PAYLOADS,
@ -24,6 +24,7 @@ async function makeTempLayout(prefix: string) {
}
afterEach(async () => {
configureFsSafePython({ mode: "auto", pythonPath: undefined });
await Promise.all(tempDirs.splice(0).map((dir) => fsp.rm(dir, { force: true, recursive: true })));
});
@ -242,4 +243,18 @@ describe("write, move, and delete boundary bypass attempts", () => {
}
await expectNoOutsideWrite(layout);
}, 15000);
it.runIf(process.platform !== "win32")("keeps literal '..'-prefixed paths available when the helper is disabled", async () => {
configureFsSafePython({ mode: "off" });
const layout = await makeTempLayout("fs-safe-write-helper-off-literal");
const safeRoot = await openRoot(layout.root);
await safeRoot.write("..%2fpwned.txt", "literal");
await expect(safeRoot.stat("..%2fpwned.txt")).resolves.toMatchObject({ isFile: true });
await safeRoot.remove("..%2fpwned.txt");
await expect(fsp.stat(path.join(layout.root, "..%2fpwned.txt"))).rejects.toMatchObject({
code: "ENOENT",
});
await expectNoOutsideWrite(layout);
});
});