fix(temp): preserve workspace leaf filename contract

This commit is contained in:
Peter Steinberger 2026-05-06 21:54:15 +01:00
parent f305c8be2b
commit 5218746972
No known key found for this signature in database
2 changed files with 23 additions and 3 deletions

View File

@ -9,7 +9,6 @@ import {
type FileStoreSync,
} from "./file-store.js";
import { openRootFileSync } from "./root-file.js";
import { isSafePathSegment } from "./safe-path-segment.js";
import { registerTempPathForExit } from "./temp-cleanup.js";
export type TempWorkspaceOptions = {
@ -62,7 +61,15 @@ function resolveWorkspaceLeaf(dir: string, fileName: string): string {
function assertWorkspaceFileName(fileName: string): string {
const value = fileName.trim();
if (!isSafePathSegment(value)) {
if (
!value ||
value === "." ||
value === ".." ||
value.includes("\0") ||
value.includes("/") ||
value.includes("\\") ||
path.basename(value) !== value
) {
throw new Error(`Invalid temp workspace file name: ${JSON.stringify(fileName)}`);
}
return value;

View File

@ -15,7 +15,7 @@ import { runPinnedWriteHelper } from "../src/pinned-write.js";
import { replaceFileAtomic } from "../src/replace-file.js";
import { writeViaSiblingTempPath } from "../src/sibling-temp.js";
import { sanitizeTempFileName, tempFile } from "../src/temp-target.js";
import { tempWorkspaceSync } from "../src/private-temp-workspace.js";
import { tempWorkspace, tempWorkspaceSync } from "../src/private-temp-workspace.js";
import { __setFsSafeTestHooksForTest } from "../src/test-hooks.js";
import { movePathToTrash } from "../src/trash.js";
@ -390,6 +390,19 @@ describe("security finding regressions", () => {
}
});
it("accepts safe temp workspace leaf names with spaces and dot prefixes", async () => {
await using workspace = await tempWorkspace({
rootDir: await tempRoot("fs-safe-workspace-leaf-"),
prefix: "work-",
});
await workspace.writeText("report 2026.txt", "ok");
await workspace.writeText(".env", "TOKEN=ok");
await expect(workspace.read("report 2026.txt")).resolves.toEqual(Buffer.from("ok"));
await expect(workspace.read(".env")).resolves.toEqual(Buffer.from("TOKEN=ok"));
});
it.runIf(process.platform !== "win32")("pins sync temp workspace reads against final symlink swaps", async () => {
const base = await tempRoot("fs-safe-temp-workspace-sync-read-");
const outside = await tempRoot("fs-safe-temp-workspace-sync-outside-");