diff --git a/src/agents/sandbox-paths.test.ts b/src/agents/sandbox-paths.test.ts index b31c22a53df..20b5938ffc2 100644 --- a/src/agents/sandbox-paths.test.ts +++ b/src/agents/sandbox-paths.test.ts @@ -5,148 +5,100 @@ import { pathToFileURL } from "node:url"; import { describe, expect, it } from "vitest"; import { resolveSandboxedMediaSource } from "./sandbox-paths.js"; +async function withSandboxRoot(run: (sandboxDir: string) => Promise) { + const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "sandbox-media-")); + try { + return await run(sandboxDir); + } finally { + await fs.rm(sandboxDir, { recursive: true, force: true }); + } +} + +async function expectSandboxRejection(media: string, sandboxRoot: string, pattern: RegExp) { + await expect(resolveSandboxedMediaSource({ media, sandboxRoot })).rejects.toThrow(pattern); +} + describe("resolveSandboxedMediaSource", () => { // Group 1: /tmp paths (the bug fix) - it("allows absolute paths under os.tmpdir()", async () => { - const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "sandbox-media-")); - try { + it.each([ + { + name: "absolute paths under os.tmpdir()", + media: path.join(os.tmpdir(), "image.png"), + expected: path.join(os.tmpdir(), "image.png"), + }, + { + name: "file:// URLs pointing to os.tmpdir()", + media: pathToFileURL(path.join(os.tmpdir(), "photo.png")).href, + expected: path.join(os.tmpdir(), "photo.png"), + }, + { + name: "nested paths under os.tmpdir()", + media: path.join(os.tmpdir(), "subdir", "deep", "file.png"), + expected: path.join(os.tmpdir(), "subdir", "deep", "file.png"), + }, + ])("allows $name", async ({ media, expected }) => { + await withSandboxRoot(async (sandboxDir) => { const result = await resolveSandboxedMediaSource({ - media: path.join(os.tmpdir(), "image.png"), + media, sandboxRoot: sandboxDir, }); - expect(result).toBe(path.join(os.tmpdir(), "image.png")); - } finally { - await fs.rm(sandboxDir, { recursive: true, force: true }); - } - }); - - it("allows file:// URLs pointing to os.tmpdir()", async () => { - const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "sandbox-media-")); - try { - const tmpFile = path.join(os.tmpdir(), "photo.png"); - const fileUrl = pathToFileURL(tmpFile).href; - const result = await resolveSandboxedMediaSource({ - media: fileUrl, - sandboxRoot: sandboxDir, - }); - expect(result).toBe(tmpFile); - } finally { - await fs.rm(sandboxDir, { recursive: true, force: true }); - } - }); - - it("allows nested paths under os.tmpdir()", async () => { - const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "sandbox-media-")); - try { - const result = await resolveSandboxedMediaSource({ - media: path.join(os.tmpdir(), "subdir", "deep", "file.png"), - sandboxRoot: sandboxDir, - }); - expect(result).toBe(path.join(os.tmpdir(), "subdir", "deep", "file.png")); - } finally { - await fs.rm(sandboxDir, { recursive: true, force: true }); - } + expect(result).toBe(expected); + }); }); // Group 2: Sandbox-relative paths (existing behavior) it("resolves sandbox-relative paths", async () => { - const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "sandbox-media-")); - try { + await withSandboxRoot(async (sandboxDir) => { const result = await resolveSandboxedMediaSource({ media: "./data/file.txt", sandboxRoot: sandboxDir, }); expect(result).toBe(path.join(sandboxDir, "data", "file.txt")); - } finally { - await fs.rm(sandboxDir, { recursive: true, force: true }); - } + }); }); // Group 3: Rejections (security) - it("rejects paths outside sandbox root and tmpdir", async () => { - const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "sandbox-media-")); - try { - await expect( - resolveSandboxedMediaSource({ media: "/etc/passwd", sandboxRoot: sandboxDir }), - ).rejects.toThrow(/sandbox/i); - } finally { - await fs.rm(sandboxDir, { recursive: true, force: true }); - } - }); - - it("rejects path traversal through tmpdir", async () => { - const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "sandbox-media-")); - try { - await expect( - resolveSandboxedMediaSource({ - media: path.join(os.tmpdir(), "..", "etc", "passwd"), - sandboxRoot: sandboxDir, - }), - ).rejects.toThrow(/sandbox/i); - } finally { - await fs.rm(sandboxDir, { recursive: true, force: true }); - } - }); - - it("rejects relative traversal outside sandbox even when sandbox root is under tmpdir", async () => { - const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "sandbox-media-")); - try { - await expect( - resolveSandboxedMediaSource({ - media: "../outside-sandbox.png", - sandboxRoot: sandboxDir, - }), - ).rejects.toThrow(/sandbox/i); - } finally { - await fs.rm(sandboxDir, { recursive: true, force: true }); - } + it.each([ + { + name: "paths outside sandbox root and tmpdir", + media: "/etc/passwd", + expected: /sandbox/i, + }, + { + name: "path traversal through tmpdir", + media: path.join(os.tmpdir(), "..", "etc", "passwd"), + expected: /sandbox/i, + }, + { + name: "relative traversal outside sandbox", + media: "../outside-sandbox.png", + expected: /sandbox/i, + }, + { + name: "file:// URLs outside sandbox", + media: "file:///etc/passwd", + expected: /sandbox/i, + }, + { + name: "invalid file:// URLs", + media: "file://not a valid url\x00", + expected: /Invalid file:\/\/ URL/, + }, + ])("rejects $name", async ({ media, expected }) => { + await withSandboxRoot(async (sandboxDir) => { + await expectSandboxRejection(media, sandboxDir, expected); + }); }); it("rejects symlinked tmpdir paths escaping tmpdir", async () => { if (process.platform === "win32") { return; } - const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "sandbox-media-")); - const symlinkPath = path.join(sandboxDir, "tmp-link-escape"); - try { + await withSandboxRoot(async (sandboxDir) => { + const symlinkPath = path.join(sandboxDir, "tmp-link-escape"); await fs.symlink("/etc/passwd", symlinkPath); - await expect( - resolveSandboxedMediaSource({ - media: symlinkPath, - sandboxRoot: sandboxDir, - }), - ).rejects.toThrow(/symlink|sandbox/i); - } finally { - await fs.rm(sandboxDir, { recursive: true, force: true }); - } - }); - - it("rejects file:// URLs outside sandbox", async () => { - const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "sandbox-media-")); - try { - await expect( - resolveSandboxedMediaSource({ - media: "file:///etc/passwd", - sandboxRoot: sandboxDir, - }), - ).rejects.toThrow(/sandbox/i); - } finally { - await fs.rm(sandboxDir, { recursive: true, force: true }); - } - }); - - it("throws on invalid file:// URLs", async () => { - const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "sandbox-media-")); - try { - await expect( - resolveSandboxedMediaSource({ - media: "file://not a valid url\x00", - sandboxRoot: sandboxDir, - }), - ).rejects.toThrow(/Invalid file:\/\/ URL/); - } finally { - await fs.rm(sandboxDir, { recursive: true, force: true }); - } + await expectSandboxRejection(symlinkPath, sandboxDir, /symlink|sandbox/i); + }); }); // Group 4: Passthrough