diff --git a/src/node-host/invoke-system-run.test.ts b/src/node-host/invoke-system-run.test.ts index 75a2c286e2f..f83d3f172c8 100644 --- a/src/node-host/invoke-system-run.test.ts +++ b/src/node-host/invoke-system-run.test.ts @@ -617,33 +617,91 @@ describe("handleSystemRunInvoke mac app exec host routing", () => { }, ); - it("handles transparent env wrappers in allowlist mode", async () => { - const { runCommand, sendInvokeResult } = await runSystemInvoke({ + it("handles transparent and semantic env wrappers in allowlist mode", async () => { + const transparent = await runSystemInvoke({ preferMacAppExecHost: false, security: "allowlist", command: ["env", "tr", "a", "b"], }); if (process.platform === "win32") { - expect(runCommand).not.toHaveBeenCalled(); - expectInvokeErrorMessage(sendInvokeResult, { message: "allowlist miss" }); - return; + expect(transparent.runCommand).not.toHaveBeenCalled(); + expectInvokeErrorMessage(transparent.sendInvokeResult, { message: "allowlist miss" }); + } else { + const runArgs = vi.mocked(transparent.runCommand).mock.calls[0]?.[0] as string[] | undefined; + expect(runArgs).toBeDefined(); + expect(runArgs?.[0]).toMatch(/(^|[/\\])tr$/); + expect(runArgs?.slice(1)).toEqual(["a", "b"]); + expectInvokeOk(transparent.sendInvokeResult); } - const runArgs = vi.mocked(runCommand).mock.calls[0]?.[0] as string[] | undefined; - expect(runArgs).toBeDefined(); - expect(runArgs?.[0]).toMatch(/(^|[/\\])tr$/); - expect(runArgs?.slice(1)).toEqual(["a", "b"]); - expectInvokeOk(sendInvokeResult); - }); - - it("denies semantic env wrappers in allowlist mode", async () => { - const { runCommand, sendInvokeResult } = await runSystemInvoke({ + const semantic = await runSystemInvoke({ preferMacAppExecHost: false, security: "allowlist", command: ["env", "FOO=bar", "tr", "a", "b"], }); - expect(runCommand).not.toHaveBeenCalled(); - expectInvokeErrorMessage(sendInvokeResult, { message: "allowlist miss" }); + expect(semantic.runCommand).not.toHaveBeenCalled(); + expectInvokeErrorMessage(semantic.sendInvokeResult, { message: "allowlist miss" }); + }); + + it("denies shell payload carriers in allowlist mode without explicit approval", async () => { + const shellPayloadCases: Array< + | { + label: string; + command: string[]; + ask?: "off" | "on-miss"; + message: string; + approvalRequired?: false; + } + | { + label: string; + command: string[]; + ask?: "off" | "on-miss"; + approvalRequired: true; + } + > = [ + { + label: "env -S", + command: ["env", "-S", 'sh -c "echo pwned"'], + message: "allowlist miss", + ask: "off", + }, + { + label: "semicolon chain simple command", + command: + process.platform === "win32" + ? ["cmd.exe", "/d", "/s", "/c", "openclaw status; id"] + : ["/bin/sh", "-lc", "openclaw status; id"], + approvalRequired: true, + }, + { + label: "semicolon chain path read", + command: + process.platform === "win32" + ? ["cmd.exe", "/d", "/s", "/c", "openclaw status; cat /etc/passwd"] + : ["/bin/sh", "-lc", "openclaw status; cat /etc/passwd"], + approvalRequired: true, + }, + { + label: "PowerShell encoded command", + command: ["pwsh", "-EncodedCommand", "ZQBjAGgAbwAgAHAAdwBuAGUAZAA="], + approvalRequired: true, + }, + ]; + + for (const testCase of shellPayloadCases) { + const { runCommand, sendInvokeResult, sendNodeEvent } = await runSystemInvoke({ + preferMacAppExecHost: false, + security: "allowlist", + ask: testCase.ask ?? "on-miss", + command: testCase.command, + }); + expect(runCommand, testCase.label).not.toHaveBeenCalled(); + if (testCase.approvalRequired) { + expectApprovalRequiredDenied({ sendNodeEvent, sendInvokeResult }); + } else if ("message" in testCase) { + expectInvokeErrorMessage(sendInvokeResult, { message: testCase.message }); + } + } }); it.runIf(process.platform !== "win32")( @@ -1087,48 +1145,6 @@ describe("handleSystemRunInvoke mac app exec host routing", () => { expectApprovalRequiredDenied({ sendNodeEvent, sendInvokeResult }); }); - it("denies env -S shell payloads in allowlist mode", async () => { - const { runCommand, sendInvokeResult } = await runSystemInvoke({ - preferMacAppExecHost: false, - security: "allowlist", - command: ["env", "-S", 'sh -c "echo pwned"'], - }); - expect(runCommand).not.toHaveBeenCalled(); - expectInvokeErrorMessage(sendInvokeResult, { message: "allowlist miss" }); - }); - - it("denies semicolon-chained shell payloads in allowlist mode without explicit approval", async () => { - const payloads = ["openclaw status; id", "openclaw status; cat /etc/passwd"]; - for (const payload of payloads) { - const command = - process.platform === "win32" - ? ["cmd.exe", "/d", "/s", "/c", payload] - : ["/bin/sh", "-lc", payload]; - const { runCommand, sendInvokeResult } = await runSystemInvoke({ - preferMacAppExecHost: false, - security: "allowlist", - ask: "on-miss", - command, - }); - expect(runCommand, payload).not.toHaveBeenCalled(); - expectInvokeErrorMessage(sendInvokeResult, { - message: "SYSTEM_RUN_DENIED: approval required", - exact: true, - }); - } - }); - - it("denies PowerShell encoded-command payloads in allowlist mode without explicit approval", async () => { - const { runCommand, sendInvokeResult, sendNodeEvent } = await runSystemInvoke({ - preferMacAppExecHost: false, - security: "allowlist", - ask: "on-miss", - command: ["pwsh", "-EncodedCommand", "ZQBjAGgAbwAgAHAAdwBuAGUAZAA="], - }); - expect(runCommand).not.toHaveBeenCalled(); - expectApprovalRequiredDenied({ sendNodeEvent, sendInvokeResult }); - }); - it("rejects unsafe environment inputs before execution", async () => { const shellCommand = process.platform === "win32" @@ -1537,100 +1553,68 @@ describe("handleSystemRunInvoke mac app exec host routing", () => { it("keeps cmd.exe transport wrappers approval-gated on Windows", async () => { const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); - const tempDir = createFixtureDir("openclaw-cmd-wrapper-allow-"); try { - const scriptPath = path.join(tempDir, "check_mail.cmd"); - fs.writeFileSync(scriptPath, "@echo off\r\necho ok\r\n"); - - await withTempApprovalsHome({ - approvals: createAllowlistOnMissApprovals({ - agents: { - main: { - allowlist: [{ pattern: scriptPath }], - }, - }, - }), - run: async () => { - const invoke = await runSystemInvoke({ - preferMacAppExecHost: false, - command: ["cmd.exe", "/d", "/s", "/c", `${scriptPath} --limit 5`], - cwd: tempDir, - security: "allowlist", - ask: "on-miss", - isCmdExeInvocation: (argv) => { - const token = argv[0]?.trim(); - if (!token) { - return false; - } - const base = path.win32.basename(token).toLowerCase(); - return base === "cmd.exe" || base === "cmd"; - }, - }); - - expect(invoke.runCommand).not.toHaveBeenCalled(); - expectApprovalRequiredDenied({ - sendNodeEvent: invoke.sendNodeEvent, - sendInvokeResult: invoke.sendInvokeResult, - }); + for (const testCase of [ + { + name: "direct cmd.exe", + commandPrefix: ["cmd.exe", "/d", "/s", "/c"], }, - }); - } finally { - platformSpy.mockRestore(); - } - }); - - it.each([ - { - name: "keeps env cmd.exe transport wrappers approval-gated on Windows", - command: ["env", "cmd.exe", "/d", "/s", "/c"], - }, - { - name: "keeps env-assignment cmd.exe transport wrappers approval-gated on Windows", - command: ["env", "FOO=bar", "cmd.exe", "/d", "/s", "/c"], - }, - ])("$name", async ({ command }) => { - const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); - const tempDir = createFixtureDir("openclaw-env-cmd-wrapper-allow-"); - try { - const scriptPath = path.join(tempDir, "check_mail.cmd"); - fs.writeFileSync(scriptPath, "@echo off\r\necho ok\r\n"); - const wrappedCommand = [...command, `${scriptPath} --limit 5`]; - - await withTempApprovalsHome({ - approvals: createAllowlistOnMissApprovals({ - agents: { - main: { - allowlist: [{ pattern: scriptPath }], - }, - }, - }), - run: async () => { - const seenArgv: string[][] = []; - const invoke = await runSystemInvoke({ - preferMacAppExecHost: false, - command: wrappedCommand, - cwd: tempDir, - security: "allowlist", - ask: "on-miss", - isCmdExeInvocation: (argv) => { - seenArgv.push([...argv]); - const token = argv[0]?.trim(); - if (!token) { - return false; - } - const base = path.win32.basename(token).toLowerCase(); - return base === "cmd.exe" || base === "cmd"; - }, - }); - - expect(seenArgv).toContainEqual(["cmd.exe", "/d", "/s", "/c", `${scriptPath} --limit 5`]); - expect(invoke.runCommand).not.toHaveBeenCalled(); - expectApprovalRequiredDenied({ - sendNodeEvent: invoke.sendNodeEvent, - sendInvokeResult: invoke.sendInvokeResult, - }); + { + name: "env cmd.exe", + commandPrefix: ["env", "cmd.exe", "/d", "/s", "/c"], }, - }); + { + name: "env-assignment cmd.exe", + commandPrefix: ["env", "FOO=bar", "cmd.exe", "/d", "/s", "/c"], + }, + ]) { + const tempDir = createFixtureDir("openclaw-cmd-wrapper-allow-"); + const scriptPath = path.join(tempDir, "check_mail.cmd"); + fs.writeFileSync(scriptPath, "@echo off\r\necho ok\r\n"); + const command = [...testCase.commandPrefix, `${scriptPath} --limit 5`]; + + await withTempApprovalsHome({ + approvals: createAllowlistOnMissApprovals({ + agents: { + main: { + allowlist: [{ pattern: scriptPath }], + }, + }, + }), + run: async () => { + const seenArgv: string[][] = []; + const invoke = await runSystemInvoke({ + preferMacAppExecHost: false, + command, + cwd: tempDir, + security: "allowlist", + ask: "on-miss", + isCmdExeInvocation: (argv) => { + seenArgv.push([...argv]); + const token = argv[0]?.trim(); + if (!token) { + return false; + } + const base = path.win32.basename(token).toLowerCase(); + return base === "cmd.exe" || base === "cmd"; + }, + }); + + expect(seenArgv, testCase.name).toContainEqual([ + "cmd.exe", + "/d", + "/s", + "/c", + `${scriptPath} --limit 5`, + ]); + expect(invoke.runCommand, testCase.name).not.toHaveBeenCalled(); + expectApprovalRequiredDenied({ + sendNodeEvent: invoke.sendNodeEvent, + sendInvokeResult: invoke.sendInvokeResult, + }); + }, + }); + } } finally { platformSpy.mockRestore(); }