mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 21:54:32 +00:00
test(sandbox): share sandbox-root setup across path cases
This commit is contained in:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user