From c9593c4c8771e27e05883ae790be0a6280344a28 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 21 Feb 2026 23:24:03 +0000 Subject: [PATCH] test(sandbox): table-drive bind and network validation cases --- .../sandbox/validate-sandbox-security.test.ts | 95 ++++++++++++------- 1 file changed, 62 insertions(+), 33 deletions(-) diff --git a/src/agents/sandbox/validate-sandbox-security.test.ts b/src/agents/sandbox/validate-sandbox-security.test.ts index 247b48b15f0..1c3e3fe0676 100644 --- a/src/agents/sandbox/validate-sandbox-security.test.ts +++ b/src/agents/sandbox/validate-sandbox-security.test.ts @@ -11,6 +11,10 @@ import { validateSandboxSecurity, } from "./validate-sandbox-security.js"; +function expectBindMountsToThrow(binds: string[], expected: RegExp, label: string) { + expect(() => validateBindMounts(binds), label).toThrow(expected); +} + describe("getBlockedBindReason", () => { it("blocks common Docker socket directories", () => { expect(getBlockedBindReason("/run:/run")).toEqual(expect.objectContaining({ kind: "targets" })); @@ -41,39 +45,58 @@ describe("validateBindMounts", () => { expect(() => validateBindMounts([])).not.toThrow(); }); - it("blocks /etc mount", () => { - expect(() => validateBindMounts(["/etc/passwd:/mnt/passwd:ro"])).toThrow( - /blocked path "\/etc"/, - ); + it("blocks dangerous bind source paths", () => { + const cases = [ + { + name: "etc mount", + binds: ["/etc/passwd:/mnt/passwd:ro"], + expected: /blocked path "\/etc"/, + }, + { + name: "proc mount", + binds: ["/proc:/proc:ro"], + expected: /blocked path "\/proc"/, + }, + { + name: "docker socket in /var/run", + binds: ["/var/run/docker.sock:/var/run/docker.sock"], + expected: /docker\.sock/, + }, + { + name: "docker socket in /run", + binds: ["/run/docker.sock:/run/docker.sock"], + expected: /docker\.sock/, + }, + { + name: "parent /run mount", + binds: ["/run:/run"], + expected: /blocked path/, + }, + { + name: "parent /var/run mount", + binds: ["/var/run:/var/run"], + expected: /blocked path/, + }, + { + name: "traversal into /etc", + binds: ["/home/user/../../etc/shadow:/mnt/shadow"], + expected: /blocked path "\/etc"/, + }, + { + name: "double-slash normalization into /etc", + binds: ["//etc//passwd:/mnt/passwd"], + expected: /blocked path "\/etc"/, + }, + ] as const; + for (const testCase of cases) { + expectBindMountsToThrow([...testCase.binds], testCase.expected, testCase.name); + } }); - it("blocks /proc mount", () => { - expect(() => validateBindMounts(["/proc:/proc:ro"])).toThrow(/blocked path "\/proc"/); - }); - - it("blocks Docker socket mounts (/var/run + /run)", () => { - expect(() => validateBindMounts(["/var/run/docker.sock:/var/run/docker.sock"])).toThrow( - /docker\.sock/, - ); - expect(() => validateBindMounts(["/run/docker.sock:/run/docker.sock"])).toThrow(/docker\.sock/); - }); - - it("blocks parent mounts that would expose the Docker socket", () => { - expect(() => validateBindMounts(["/run:/run"])).toThrow(/blocked path/); - expect(() => validateBindMounts(["/var/run:/var/run"])).toThrow(/blocked path/); + it("allows parent mounts that are not blocked", () => { expect(() => validateBindMounts(["/var:/var"])).not.toThrow(); }); - it("blocks paths with .. traversal to dangerous directories", () => { - expect(() => validateBindMounts(["/home/user/../../etc/shadow:/mnt/shadow"])).toThrow( - /blocked path "\/etc"/, - ); - }); - - it("blocks paths with double slashes normalizing to dangerous dirs", () => { - expect(() => validateBindMounts(["//etc//passwd:/mnt/passwd"])).toThrow(/blocked path "\/etc"/); - }); - it("blocks symlink escapes into blocked directories", () => { const dir = mkdtempSync(join(tmpdir(), "openclaw-sbx-")); const link = join(dir, "etc-link"); @@ -90,9 +113,10 @@ describe("validateBindMounts", () => { }); it("rejects non-absolute source paths (relative or named volumes)", () => { - expect(() => validateBindMounts(["../etc/passwd:/mnt/passwd"])).toThrow(/non-absolute/); - expect(() => validateBindMounts(["etc/passwd:/mnt/passwd"])).toThrow(/non-absolute/); - expect(() => validateBindMounts(["myvol:/mnt"])).toThrow(/non-absolute/); + const cases = ["../etc/passwd:/mnt/passwd", "etc/passwd:/mnt/passwd", "myvol:/mnt"] as const; + for (const source of cases) { + expectBindMountsToThrow([source], /non-absolute/, source); + } }); }); @@ -105,8 +129,13 @@ describe("validateNetworkMode", () => { }); it("blocks host mode (case-insensitive)", () => { - expect(() => validateNetworkMode("host")).toThrow(/network mode "host" is blocked/); - expect(() => validateNetworkMode("HOST")).toThrow(/network mode "HOST" is blocked/); + const cases = [ + { mode: "host", expected: /network mode "host" is blocked/ }, + { mode: "HOST", expected: /network mode "HOST" is blocked/ }, + ] as const; + for (const testCase of cases) { + expect(() => validateNetworkMode(testCase.mode), testCase.mode).toThrow(testCase.expected); + } }); });