test(browser): dedupe fixture lifecycle and cover directory-path rejection

This commit is contained in:
Peter Steinberger
2026-02-21 19:20:35 +00:00
parent 626d8e9f62
commit ac6c344d9b

View File

@@ -1,7 +1,7 @@
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import { afterEach, describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { resolveExistingPathsWithinRoot } from "./paths.js"; import { resolveExistingPathsWithinRoot } from "./paths.js";
async function createFixtureRoot(): Promise<{ baseDir: string; uploadsDir: string }> { async function createFixtureRoot(): Promise<{ baseDir: string; uploadsDir: string }> {
@@ -11,22 +11,20 @@ async function createFixtureRoot(): Promise<{ baseDir: string; uploadsDir: strin
return { baseDir, uploadsDir }; return { baseDir, uploadsDir };
} }
async function withFixtureRoot<T>(
run: (ctx: { baseDir: string; uploadsDir: string }) => Promise<T>,
): Promise<T> {
const fixture = await createFixtureRoot();
try {
return await run(fixture);
} finally {
await fs.rm(fixture.baseDir, { recursive: true, force: true });
}
}
describe("resolveExistingPathsWithinRoot", () => { describe("resolveExistingPathsWithinRoot", () => {
const cleanupDirs = new Set<string>();
afterEach(async () => {
await Promise.all(
Array.from(cleanupDirs).map(async (dir) => {
await fs.rm(dir, { recursive: true, force: true });
}),
);
cleanupDirs.clear();
});
it("accepts existing files under the upload root", async () => { it("accepts existing files under the upload root", async () => {
const { baseDir, uploadsDir } = await createFixtureRoot(); await withFixtureRoot(async ({ uploadsDir }) => {
cleanupDirs.add(baseDir);
const nestedDir = path.join(uploadsDir, "nested"); const nestedDir = path.join(uploadsDir, "nested");
await fs.mkdir(nestedDir, { recursive: true }); await fs.mkdir(nestedDir, { recursive: true });
const filePath = path.join(nestedDir, "ok.txt"); const filePath = path.join(nestedDir, "ok.txt");
@@ -43,11 +41,10 @@ describe("resolveExistingPathsWithinRoot", () => {
expect(result.paths).toEqual([await fs.realpath(filePath)]); expect(result.paths).toEqual([await fs.realpath(filePath)]);
} }
}); });
});
it("rejects traversal outside the upload root", async () => { it("rejects traversal outside the upload root", async () => {
const { baseDir, uploadsDir } = await createFixtureRoot(); await withFixtureRoot(async ({ baseDir, uploadsDir }) => {
cleanupDirs.add(baseDir);
const outsidePath = path.join(baseDir, "outside.txt"); const outsidePath = path.join(baseDir, "outside.txt");
await fs.writeFile(outsidePath, "nope", "utf8"); await fs.writeFile(outsidePath, "nope", "utf8");
@@ -62,11 +59,10 @@ describe("resolveExistingPathsWithinRoot", () => {
expect(result.error).toContain("must stay within uploads directory"); expect(result.error).toContain("must stay within uploads directory");
} }
}); });
});
it("keeps lexical in-root paths when files do not exist yet", async () => { it("keeps lexical in-root paths when files do not exist yet", async () => {
const { baseDir, uploadsDir } = await createFixtureRoot(); await withFixtureRoot(async ({ uploadsDir }) => {
cleanupDirs.add(baseDir);
const result = await resolveExistingPathsWithinRoot({ const result = await resolveExistingPathsWithinRoot({
rootDir: uploadsDir, rootDir: uploadsDir,
requestedPaths: ["missing.txt"], requestedPaths: ["missing.txt"],
@@ -78,13 +74,30 @@ describe("resolveExistingPathsWithinRoot", () => {
expect(result.paths).toEqual([path.join(uploadsDir, "missing.txt")]); expect(result.paths).toEqual([path.join(uploadsDir, "missing.txt")]);
} }
}); });
});
it("rejects directory paths inside upload root", async () => {
await withFixtureRoot(async ({ uploadsDir }) => {
const nestedDir = path.join(uploadsDir, "nested");
await fs.mkdir(nestedDir, { recursive: true });
const result = await resolveExistingPathsWithinRoot({
rootDir: uploadsDir,
requestedPaths: ["nested"],
scopeLabel: "uploads directory",
});
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toContain("regular non-symlink file");
}
});
});
it.runIf(process.platform !== "win32")( it.runIf(process.platform !== "win32")(
"rejects symlink escapes outside upload root", "rejects symlink escapes outside upload root",
async () => { async () => {
const { baseDir, uploadsDir } = await createFixtureRoot(); await withFixtureRoot(async ({ baseDir, uploadsDir }) => {
cleanupDirs.add(baseDir);
const outsidePath = path.join(baseDir, "secret.txt"); const outsidePath = path.join(baseDir, "secret.txt");
await fs.writeFile(outsidePath, "secret", "utf8"); await fs.writeFile(outsidePath, "secret", "utf8");
const symlinkPath = path.join(uploadsDir, "leak.txt"); const symlinkPath = path.join(uploadsDir, "leak.txt");
@@ -100,6 +113,7 @@ describe("resolveExistingPathsWithinRoot", () => {
if (!result.ok) { if (!result.ok) {
expect(result.error).toContain("regular non-symlink file"); expect(result.error).toContain("regular non-symlink file");
} }
});
}, },
); );
}); });