From ac6c344d9ba522f333a06f509c49f0f67122e348 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 21 Feb 2026 19:20:35 +0000 Subject: [PATCH] test(browser): dedupe fixture lifecycle and cover directory-path rejection --- src/browser/paths.test.ts | 170 +++++++++++++++++++++----------------- 1 file changed, 92 insertions(+), 78 deletions(-) diff --git a/src/browser/paths.test.ts b/src/browser/paths.test.ts index 0ece74c4893..6719961d6c5 100644 --- a/src/browser/paths.test.ts +++ b/src/browser/paths.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { resolveExistingPathsWithinRoot } from "./paths.js"; async function createFixtureRoot(): Promise<{ baseDir: string; uploadsDir: string }> { @@ -11,88 +11,79 @@ async function createFixtureRoot(): Promise<{ baseDir: string; uploadsDir: strin return { baseDir, uploadsDir }; } +async function withFixtureRoot( + run: (ctx: { baseDir: string; uploadsDir: string }) => Promise, +): Promise { + const fixture = await createFixtureRoot(); + try { + return await run(fixture); + } finally { + await fs.rm(fixture.baseDir, { recursive: true, force: true }); + } +} + describe("resolveExistingPathsWithinRoot", () => { - const cleanupDirs = new Set(); - - 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 () => { - const { baseDir, uploadsDir } = await createFixtureRoot(); - cleanupDirs.add(baseDir); - - const nestedDir = path.join(uploadsDir, "nested"); - await fs.mkdir(nestedDir, { recursive: true }); - const filePath = path.join(nestedDir, "ok.txt"); - await fs.writeFile(filePath, "ok", "utf8"); - - const result = await resolveExistingPathsWithinRoot({ - rootDir: uploadsDir, - requestedPaths: [filePath], - scopeLabel: "uploads directory", - }); - - expect(result.ok).toBe(true); - if (result.ok) { - expect(result.paths).toEqual([await fs.realpath(filePath)]); - } - }); - - it("rejects traversal outside the upload root", async () => { - const { baseDir, uploadsDir } = await createFixtureRoot(); - cleanupDirs.add(baseDir); - - const outsidePath = path.join(baseDir, "outside.txt"); - await fs.writeFile(outsidePath, "nope", "utf8"); - - const result = await resolveExistingPathsWithinRoot({ - rootDir: uploadsDir, - requestedPaths: ["../outside.txt"], - scopeLabel: "uploads directory", - }); - - expect(result.ok).toBe(false); - if (!result.ok) { - expect(result.error).toContain("must stay within uploads directory"); - } - }); - - it("keeps lexical in-root paths when files do not exist yet", async () => { - const { baseDir, uploadsDir } = await createFixtureRoot(); - cleanupDirs.add(baseDir); - - const result = await resolveExistingPathsWithinRoot({ - rootDir: uploadsDir, - requestedPaths: ["missing.txt"], - scopeLabel: "uploads directory", - }); - - expect(result.ok).toBe(true); - if (result.ok) { - expect(result.paths).toEqual([path.join(uploadsDir, "missing.txt")]); - } - }); - - it.runIf(process.platform !== "win32")( - "rejects symlink escapes outside upload root", - async () => { - const { baseDir, uploadsDir } = await createFixtureRoot(); - cleanupDirs.add(baseDir); - - const outsidePath = path.join(baseDir, "secret.txt"); - await fs.writeFile(outsidePath, "secret", "utf8"); - const symlinkPath = path.join(uploadsDir, "leak.txt"); - await fs.symlink(outsidePath, symlinkPath); + await withFixtureRoot(async ({ uploadsDir }) => { + const nestedDir = path.join(uploadsDir, "nested"); + await fs.mkdir(nestedDir, { recursive: true }); + const filePath = path.join(nestedDir, "ok.txt"); + await fs.writeFile(filePath, "ok", "utf8"); const result = await resolveExistingPathsWithinRoot({ rootDir: uploadsDir, - requestedPaths: ["leak.txt"], + requestedPaths: [filePath], + scopeLabel: "uploads directory", + }); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.paths).toEqual([await fs.realpath(filePath)]); + } + }); + }); + + it("rejects traversal outside the upload root", async () => { + await withFixtureRoot(async ({ baseDir, uploadsDir }) => { + const outsidePath = path.join(baseDir, "outside.txt"); + await fs.writeFile(outsidePath, "nope", "utf8"); + + const result = await resolveExistingPathsWithinRoot({ + rootDir: uploadsDir, + requestedPaths: ["../outside.txt"], + scopeLabel: "uploads directory", + }); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toContain("must stay within uploads directory"); + } + }); + }); + + it("keeps lexical in-root paths when files do not exist yet", async () => { + await withFixtureRoot(async ({ uploadsDir }) => { + const result = await resolveExistingPathsWithinRoot({ + rootDir: uploadsDir, + requestedPaths: ["missing.txt"], + scopeLabel: "uploads directory", + }); + + expect(result.ok).toBe(true); + if (result.ok) { + 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", }); @@ -100,6 +91,29 @@ describe("resolveExistingPathsWithinRoot", () => { if (!result.ok) { expect(result.error).toContain("regular non-symlink file"); } + }); + }); + + it.runIf(process.platform !== "win32")( + "rejects symlink escapes outside upload root", + async () => { + await withFixtureRoot(async ({ baseDir, uploadsDir }) => { + const outsidePath = path.join(baseDir, "secret.txt"); + await fs.writeFile(outsidePath, "secret", "utf8"); + const symlinkPath = path.join(uploadsDir, "leak.txt"); + await fs.symlink(outsidePath, symlinkPath); + + const result = await resolveExistingPathsWithinRoot({ + rootDir: uploadsDir, + requestedPaths: ["leak.txt"], + scopeLabel: "uploads directory", + }); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toContain("regular non-symlink file"); + } + }); }, ); });