test(sandbox): share sandbox-root setup across path cases

This commit is contained in:
Peter Steinberger
2026-02-21 23:38:30 +00:00
parent 548c227411
commit 8922cb4085

View File

@@ -5,148 +5,100 @@ import { pathToFileURL } from "node:url";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { resolveSandboxedMediaSource } from "./sandbox-paths.js"; import { resolveSandboxedMediaSource } from "./sandbox-paths.js";
async function withSandboxRoot<T>(run: (sandboxDir: string) => Promise<T>) {
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", () => { describe("resolveSandboxedMediaSource", () => {
// Group 1: /tmp paths (the bug fix) // Group 1: /tmp paths (the bug fix)
it("allows absolute paths under os.tmpdir()", async () => { it.each([
const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "sandbox-media-")); {
try { 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({ const result = await resolveSandboxedMediaSource({
media: path.join(os.tmpdir(), "image.png"), media,
sandboxRoot: sandboxDir, sandboxRoot: sandboxDir,
}); });
expect(result).toBe(path.join(os.tmpdir(), "image.png")); expect(result).toBe(expected);
} 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 });
}
}); });
// Group 2: Sandbox-relative paths (existing behavior) // Group 2: Sandbox-relative paths (existing behavior)
it("resolves sandbox-relative paths", async () => { it("resolves sandbox-relative paths", async () => {
const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "sandbox-media-")); await withSandboxRoot(async (sandboxDir) => {
try {
const result = await resolveSandboxedMediaSource({ const result = await resolveSandboxedMediaSource({
media: "./data/file.txt", media: "./data/file.txt",
sandboxRoot: sandboxDir, sandboxRoot: sandboxDir,
}); });
expect(result).toBe(path.join(sandboxDir, "data", "file.txt")); expect(result).toBe(path.join(sandboxDir, "data", "file.txt"));
} finally { });
await fs.rm(sandboxDir, { recursive: true, force: true });
}
}); });
// Group 3: Rejections (security) // Group 3: Rejections (security)
it("rejects paths outside sandbox root and tmpdir", async () => { it.each([
const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "sandbox-media-")); {
try { name: "paths outside sandbox root and tmpdir",
await expect( media: "/etc/passwd",
resolveSandboxedMediaSource({ media: "/etc/passwd", sandboxRoot: sandboxDir }), expected: /sandbox/i,
).rejects.toThrow(/sandbox/i); },
} finally { {
await fs.rm(sandboxDir, { recursive: true, force: true }); name: "path traversal through tmpdir",
} media: path.join(os.tmpdir(), "..", "etc", "passwd"),
}); expected: /sandbox/i,
},
it("rejects path traversal through tmpdir", async () => { {
const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "sandbox-media-")); name: "relative traversal outside sandbox",
try { media: "../outside-sandbox.png",
await expect( expected: /sandbox/i,
resolveSandboxedMediaSource({ },
media: path.join(os.tmpdir(), "..", "etc", "passwd"), {
sandboxRoot: sandboxDir, name: "file:// URLs outside sandbox",
}), media: "file:///etc/passwd",
).rejects.toThrow(/sandbox/i); expected: /sandbox/i,
} finally { },
await fs.rm(sandboxDir, { recursive: true, force: true }); {
} name: "invalid file:// URLs",
}); media: "file://not a valid url\x00",
expected: /Invalid file:\/\/ URL/,
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-")); ])("rejects $name", async ({ media, expected }) => {
try { await withSandboxRoot(async (sandboxDir) => {
await expect( await expectSandboxRejection(media, sandboxDir, expected);
resolveSandboxedMediaSource({ });
media: "../outside-sandbox.png",
sandboxRoot: sandboxDir,
}),
).rejects.toThrow(/sandbox/i);
} finally {
await fs.rm(sandboxDir, { recursive: true, force: true });
}
}); });
it("rejects symlinked tmpdir paths escaping tmpdir", async () => { it("rejects symlinked tmpdir paths escaping tmpdir", async () => {
if (process.platform === "win32") { if (process.platform === "win32") {
return; return;
} }
const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "sandbox-media-")); await withSandboxRoot(async (sandboxDir) => {
const symlinkPath = path.join(sandboxDir, "tmp-link-escape"); const symlinkPath = path.join(sandboxDir, "tmp-link-escape");
try {
await fs.symlink("/etc/passwd", symlinkPath); await fs.symlink("/etc/passwd", symlinkPath);
await expect( await expectSandboxRejection(symlinkPath, sandboxDir, /symlink|sandbox/i);
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 });
}
}); });
// Group 4: Passthrough // Group 4: Passthrough