diff --git a/src/infra/shell-env.test.ts b/src/infra/shell-env.test.ts index 3c443a5c4..9614f845f 100644 --- a/src/infra/shell-env.test.ts +++ b/src/infra/shell-env.test.ts @@ -1,3 +1,4 @@ +import fs from "node:fs"; import { describe, expect, it, vi } from "vitest"; import { getShellPathFromLoginShell, @@ -25,6 +26,18 @@ describe("shell env fallback", () => { return { first, second }; } + function runShellEnvFallbackForShell(shell: string) { + const env: NodeJS.ProcessEnv = { SHELL: shell }; + const exec = vi.fn(() => Buffer.from("OPENAI_API_KEY=from-shell\0")); + const res = loadShellEnvFallback({ + enabled: true, + env, + expectedKeys: ["OPENAI_API_KEY"], + exec: exec as unknown as Parameters[0]["exec"], + }); + return { res, exec }; + } + it("is disabled by default", () => { expect(shouldEnableShellEnvFallback({} as NodeJS.ProcessEnv)).toBe(false); expect(shouldEnableShellEnvFallback({ OPENCLAW_LOAD_SHELL_ENV: "0" })).toBe(false); @@ -122,15 +135,7 @@ describe("shell env fallback", () => { }); it("falls back to /bin/sh when SHELL is non-absolute", () => { - const env: NodeJS.ProcessEnv = { SHELL: "zsh" }; - const exec = vi.fn(() => Buffer.from("OPENAI_API_KEY=from-shell\0")); - - const res = loadShellEnvFallback({ - enabled: true, - env, - expectedKeys: ["OPENAI_API_KEY"], - exec: exec as unknown as Parameters[0]["exec"], - }); + const { res, exec } = runShellEnvFallbackForShell("zsh"); expect(res.ok).toBe(true); expect(exec).toHaveBeenCalledTimes(1); @@ -138,21 +143,27 @@ describe("shell env fallback", () => { }); it("falls back to /bin/sh when SHELL points to an untrusted path", () => { - const env: NodeJS.ProcessEnv = { SHELL: "/tmp/evil-shell" }; - const exec = vi.fn(() => Buffer.from("OPENAI_API_KEY=from-shell\0")); - - const res = loadShellEnvFallback({ - enabled: true, - env, - expectedKeys: ["OPENAI_API_KEY"], - exec: exec as unknown as Parameters[0]["exec"], - }); + const { res, exec } = runShellEnvFallbackForShell("/tmp/evil-shell"); expect(res.ok).toBe(true); expect(exec).toHaveBeenCalledTimes(1); expect(exec).toHaveBeenCalledWith("/bin/sh", ["-l", "-c", "env -0"], expect.any(Object)); }); + it("uses trusted absolute SHELL path when executable", () => { + const accessSyncSpy = vi.spyOn(fs, "accessSync").mockImplementation(() => undefined); + try { + const trustedShell = "/usr/bin/zsh-trusted"; + const { res, exec } = runShellEnvFallbackForShell(trustedShell); + + expect(res.ok).toBe(true); + expect(exec).toHaveBeenCalledTimes(1); + expect(exec).toHaveBeenCalledWith(trustedShell, ["-l", "-c", "env -0"], expect.any(Object)); + } finally { + accessSyncSpy.mockRestore(); + } + }); + it("returns null without invoking shell on win32", () => { resetShellPathCacheForTests(); const exec = vi.fn(() => Buffer.from("PATH=/usr/local/bin:/usr/bin\0HOME=/tmp\0"));