From 48bfcb2625cf3196fe8830ab0e159a26445f3d4d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 14:13:29 +0000 Subject: [PATCH 1/5] Initial plan From 03e5cae797d1b36989b10061a81fcf624e222d7c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 14:16:16 +0000 Subject: [PATCH 2/5] Harden deny CLI wrapper generation --- packages/runtime/src/band-server.ts | 53 +++++++++------- .../runtime/test/unit/band-server.test.ts | 61 +++++++++++++++++++ 2 files changed, 91 insertions(+), 23 deletions(-) create mode 100644 packages/runtime/test/unit/band-server.test.ts diff --git a/packages/runtime/src/band-server.ts b/packages/runtime/src/band-server.ts index 211c568..95157f9 100644 --- a/packages/runtime/src/band-server.ts +++ b/packages/runtime/src/band-server.ts @@ -153,6 +153,10 @@ function writeFile(path: string, content: string): void { shell(`echo '${b64}' | base64 -d > ${path}`); } +function shellSingleQuote(value: string): string { + return `'${value.replace(/'/g, "'\\''")}'`; +} + // ── Firewall ────────────────────────────────────────────────────────── function setupFirewall( @@ -325,6 +329,31 @@ const ESSENTIAL_COMMANDS = [ "sudo", "su", ]; +export function buildCliWrapper(cmd: string, realPath: string, denyPats: string[]): string { + const logLine = `[ -n "\$BAND_OPS_FILE" ] && echo "${cmd} $*" >> "\$BAND_OPS_FILE"`; + + if (denyPats.length > 0) { + const patternArray = denyPats.map(shellSingleQuote).join(" "); + return `#!/bin/bash +FULL_CMD="${cmd} $*" +DENY_PATTERNS=(${patternArray}) +for P in "\${DENY_PATTERNS[@]}"; do + if [[ "$FULL_CMD" == $P ]]; then + echo "DENIED: $FULL_CMD" >&2 + exit 126 + fi +done +${logLine} +exec ${realPath} "$@" +`; + } + + return `#!/bin/bash +${logLine} +exec ${realPath} "$@" +`; +} + function setupCliWrappers( wrapperDir: string, allowPatterns: string[], @@ -355,29 +384,7 @@ function setupCliWrappers( if (!realPath) continue; const denyPats = denyByCmd.get(cmd) || []; - const logLine = `[ -n "\$BAND_OPS_FILE" ] && echo "${cmd} $*" >> "\$BAND_OPS_FILE"`; - - let wrapper: string; - if (denyPats.length > 0) { - const patternArray = denyPats.map(p => `"${p.replace(/"/g, '\\"')}"`).join(" "); - wrapper = `#!/bin/bash -FULL_CMD="${cmd} $*" -DENY_PATTERNS=(${patternArray}) -for P in "\${DENY_PATTERNS[@]}"; do - if eval "[[ \\"\\$FULL_CMD\\" == \\$P ]]" 2>/dev/null; then - echo "DENIED: $FULL_CMD" >&2 - exit 126 - fi -done -${logLine} -exec ${realPath} "$@" -`; - } else { - wrapper = `#!/bin/bash -${logLine} -exec ${realPath} "$@" -`; - } + const wrapper = buildCliWrapper(cmd, realPath, denyPats); writeFile(`${wrapperDir}/${cmd}`, wrapper); shell(`chmod +x ${wrapperDir}/${cmd}`); diff --git a/packages/runtime/test/unit/band-server.test.ts b/packages/runtime/test/unit/band-server.test.ts new file mode 100644 index 0000000..ee60c83 --- /dev/null +++ b/packages/runtime/test/unit/band-server.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, test } from "bun:test"; +import { chmodSync, existsSync, mkdtempSync, writeFileSync } from "fs"; +import { tmpdir } from "os"; +import { join } from "path"; + +async function importBandServer() { + const originalSpawnSync = Bun.spawnSync; + (Bun as any).spawnSync = ((cmd: string[]) => { + if (cmd[0] === "id" && (cmd[1] === "-u" || cmd[1] === "-g")) { + return { + exitCode: 0, + stdout: Buffer.from("1000\n"), + stderr: Buffer.from(""), + }; + } + return originalSpawnSync(cmd as any); + }) as any; + + try { + return await import(`../../src/band-server.ts?test=${Date.now()}`); + } finally { + (Bun as any).spawnSync = originalSpawnSync; + } +} + +describe("band-server CLI wrappers", () => { + test("quotes deny patterns so command substitutions are not executed", async () => { + const { buildCliWrapper } = await importBandServer(); + const dir = mkdtempSync(join(tmpdir(), "band-wrapper-test-")); + const marker = join(dir, "pwned"); + const realCmd = join(dir, "real-foo"); + const wrapperPath = join(dir, "foo"); + + writeFileSync(realCmd, "#!/bin/bash\nexit 0\n"); + chmodSync(realCmd, 0o755); + + const wrapper = buildCliWrapper("foo", realCmd, [`foo$(touch ${marker})*`]); + expect(wrapper).not.toContain("eval"); + expect(wrapper).toContain('if [[ "$FULL_CMD" == $P ]]; then'); + + writeFileSync(wrapperPath, wrapper); + chmodSync(wrapperPath, 0o755); + + const proc = Bun.spawnSync(["bash", wrapperPath, "safe"]); + + expect(proc.exitCode).toBe(0); + expect(existsSync(marker)).toBe(false); + }); + + test("preserves shell metacharacters in deny patterns as literal array values", async () => { + const { buildCliWrapper } = await importBandServer(); + + const wrapper = buildCliWrapper("foo", "/bin/true", [ + "foo *", + "foo'bar*", + "foo`echo pwned`*", + ]); + + expect(wrapper).toContain("DENY_PATTERNS=('foo *' 'foo'\\''bar*' 'foo`echo pwned`*')"); + }); +}); From dd87c357d963781e681212d8662c190aeff295a6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 14:17:48 +0000 Subject: [PATCH 3/5] Clarify deny wrapper regression test --- packages/runtime/test/unit/band-server.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime/test/unit/band-server.test.ts b/packages/runtime/test/unit/band-server.test.ts index ee60c83..29a8254 100644 --- a/packages/runtime/test/unit/band-server.test.ts +++ b/packages/runtime/test/unit/band-server.test.ts @@ -34,7 +34,7 @@ describe("band-server CLI wrappers", () => { writeFileSync(realCmd, "#!/bin/bash\nexit 0\n"); chmodSync(realCmd, 0o755); - const wrapper = buildCliWrapper("foo", realCmd, [`foo$(touch ${marker})*`]); + const wrapper = buildCliWrapper("foo", realCmd, ["foo$(touch " + marker + ")*"]); expect(wrapper).not.toContain("eval"); expect(wrapper).toContain('if [[ "$FULL_CMD" == $P ]]; then'); From 4212d93162f9c668547c4dce4784101c01d1d086 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 14:19:00 +0000 Subject: [PATCH 4/5] Assert quoted malicious deny pattern --- packages/runtime/test/unit/band-server.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/runtime/test/unit/band-server.test.ts b/packages/runtime/test/unit/band-server.test.ts index 29a8254..5583eb1 100644 --- a/packages/runtime/test/unit/band-server.test.ts +++ b/packages/runtime/test/unit/band-server.test.ts @@ -36,6 +36,7 @@ describe("band-server CLI wrappers", () => { const wrapper = buildCliWrapper("foo", realCmd, ["foo$(touch " + marker + ")*"]); expect(wrapper).not.toContain("eval"); + expect(wrapper).toContain("'foo$(touch " + marker + ")*'"); expect(wrapper).toContain('if [[ "$FULL_CMD" == $P ]]; then'); writeFileSync(wrapperPath, wrapper); From c3399b8aa6c746a783f4fabe8d45ff90830760ea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 14:20:16 +0000 Subject: [PATCH 5/5] Clean up deny wrapper tests --- packages/runtime/src/band-server.ts | 4 ++-- packages/runtime/test/unit/band-server.test.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/runtime/src/band-server.ts b/packages/runtime/src/band-server.ts index 95157f9..f6d3e66 100644 --- a/packages/runtime/src/band-server.ts +++ b/packages/runtime/src/band-server.ts @@ -333,10 +333,10 @@ export function buildCliWrapper(cmd: string, realPath: string, denyPats: string[ const logLine = `[ -n "\$BAND_OPS_FILE" ] && echo "${cmd} $*" >> "\$BAND_OPS_FILE"`; if (denyPats.length > 0) { - const patternArray = denyPats.map(shellSingleQuote).join(" "); + const quotedPatterns = denyPats.map(shellSingleQuote).join(" "); return `#!/bin/bash FULL_CMD="${cmd} $*" -DENY_PATTERNS=(${patternArray}) +DENY_PATTERNS=(${quotedPatterns}) for P in "\${DENY_PATTERNS[@]}"; do if [[ "$FULL_CMD" == $P ]]; then echo "DENIED: $FULL_CMD" >&2 diff --git a/packages/runtime/test/unit/band-server.test.ts b/packages/runtime/test/unit/band-server.test.ts index 5583eb1..2974bf0 100644 --- a/packages/runtime/test/unit/band-server.test.ts +++ b/packages/runtime/test/unit/band-server.test.ts @@ -17,7 +17,7 @@ async function importBandServer() { }) as any; try { - return await import(`../../src/band-server.ts?test=${Date.now()}`); + return await import("../../src/band-server"); } finally { (Bun as any).spawnSync = originalSpawnSync; }