From 86251f43916d8210d38d8f69c9fb0b0070a88fdf Mon Sep 17 00:00:00 2001 From: Pavan Kumar Gondhi Date: Fri, 1 May 2026 18:35:03 +0530 Subject: [PATCH] fix: block workspace CLOUDSDK_PYTHON override and always set trusted interpreter for gcloud (#74492) * fix: address issue * docs: add changelog entry for PR merge --- CHANGELOG.md | 1 + src/hooks/gmail-setup-utils.test.ts | 84 +++++++++++++++++++++++++++++ src/hooks/gmail-setup-utils.ts | 10 ++-- src/infra/dotenv.test.ts | 3 ++ src/infra/dotenv.ts | 1 + 5 files changed, 92 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03d7e298131..03a547839f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- fix: block workspace CLOUDSDK_PYTHON override and always set trusted interpreter for gcloud. (#74492) Thanks @pgondhi987. - fix(infra): block ambient Homebrew env vars from brew resolution. (#74463) Thanks @pgondhi987. - Thinking/providers: resolve bundled provider thinking profiles through lightweight provider policy artifacts when startup-lazy providers are not active, so OpenAI Codex GPT-5.x keeps xhigh available in Gateway session validation. Fixes #74796. Thanks @maxschachere. - Security/Windows: ignore workspace `.env` system-path variables and resolve stale-process `taskkill.exe` from the validated Windows install root, preventing repository-local env files from redirecting cleanup helpers. Thanks @pgondhi987. diff --git a/src/hooks/gmail-setup-utils.test.ts b/src/hooks/gmail-setup-utils.test.ts index bf63651e18f..d7b75e79f02 100644 --- a/src/hooks/gmail-setup-utils.test.ts +++ b/src/hooks/gmail-setup-utils.test.ts @@ -7,6 +7,7 @@ import { ensureTailscaleEndpoint, resetGmailSetupUtilsCachesForTest, resolvePythonExecutablePath, + runGcloud, } from "./gmail-setup-utils.js"; const itUnix = process.platform === "win32" ? it.skip : it; @@ -63,6 +64,89 @@ describe("resolvePythonExecutablePath", () => { ); }); +describe("runGcloud", () => { + itUnix( + "overrides an inherited CLOUDSDK_PYTHON value with a resolved interpreter", + async () => { + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gcloud-python-")); + try { + const realPython = path.join(tmp, "python-real"); + await fs.writeFile(realPython, "#!/bin/sh\nexit 0\n", "utf-8"); + await fs.chmod(realPython, 0o755); + + const shimDir = path.join(tmp, "shims"); + await fs.mkdir(shimDir, { recursive: true }); + const shim = path.join(shimDir, "python3"); + await fs.writeFile(shim, "#!/bin/sh\nexit 0\n", "utf-8"); + await fs.chmod(shim, 0o755); + + await withEnvAsync( + { + CLOUDSDK_PYTHON: path.join(tmp, "evil", "python"), + PATH: `${shimDir}${path.delimiter}/usr/bin`, + }, + async () => { + runCommandWithTimeoutMock + .mockResolvedValueOnce({ + stdout: `${realPython}\n`, + stderr: "", + code: 0, + signal: null, + killed: false, + }) + .mockResolvedValueOnce({ + stdout: "", + stderr: "", + code: 0, + signal: null, + killed: false, + }); + + await runGcloud(["config", "list"]); + + expect(runCommandWithTimeoutMock).toHaveBeenLastCalledWith( + ["gcloud", "config", "list"], + { + timeoutMs: 120_000, + env: { CLOUDSDK_PYTHON: realPython }, + }, + ); + }, + ); + } finally { + await fs.rm(tmp, { recursive: true, force: true }); + } + }, + 60_000, + ); + + itUnix("unsets inherited CLOUDSDK_PYTHON when no trusted interpreter is found", async () => { + await withEnvAsync( + { + CLOUDSDK_PYTHON: "/tmp/attacker-python", + PATH: "", + }, + async () => { + runCommandWithTimeoutMock.mockResolvedValueOnce({ + stdout: "", + stderr: "", + code: 0, + signal: null, + killed: false, + }); + + await runGcloud(["config", "list"]); + + expect(runCommandWithTimeoutMock).toHaveBeenCalledTimes(1); + expect(runCommandWithTimeoutMock).toHaveBeenCalledWith(["gcloud", "config", "list"], { + timeoutMs: 120_000, + env: { CLOUDSDK_PYTHON: undefined }, + }); + }, + ); + }); +}); + describe("ensureTailscaleEndpoint", () => { it("includes stdout and exit code when tailscale serve fails", async () => { runCommandWithTimeoutMock diff --git a/src/hooks/gmail-setup-utils.ts b/src/hooks/gmail-setup-utils.ts index f901b947bcb..4c74043c843 100644 --- a/src/hooks/gmail-setup-utils.ts +++ b/src/hooks/gmail-setup-utils.ts @@ -145,14 +145,10 @@ export async function resolvePythonExecutablePath(): Promise return undefined; } -async function gcloudEnv(): Promise { - if (process.env.CLOUDSDK_PYTHON) { - return undefined; - } +async function gcloudEnv(): Promise { const pythonPath = await resolvePythonExecutablePath(); - if (!pythonPath) { - return undefined; - } + // Always override inherited CLOUDSDK_PYTHON so gcloud cannot select a + // workspace-controlled interpreter. return { CLOUDSDK_PYTHON: pythonPath }; } diff --git a/src/infra/dotenv.test.ts b/src/infra/dotenv.test.ts index 2eb5a990d16..6f0907404ac 100644 --- a/src/infra/dotenv.test.ts +++ b/src/infra/dotenv.test.ts @@ -209,6 +209,7 @@ describe("loadDotEnv", () => { "OPENCLAW_STATE_DIR=./evil-state", "OPENCLAW_CONFIG_PATH=./evil-config.json", "ANTHROPIC_BASE_URL=https://evil.example.com/v1", + "CLOUDSDK_PYTHON=./attacker-python", "EXAMPLE_API_HOST=https://evil-api.example.com", "MINIMAX_API_HOST=https://evil.example.com", "HTTP_PROXY=http://evil-proxy:8080", @@ -225,6 +226,7 @@ describe("loadDotEnv", () => { delete process.env.NODE_OPTIONS; delete process.env.OPENCLAW_CONFIG_PATH; delete process.env.ANTHROPIC_BASE_URL; + delete process.env.CLOUDSDK_PYTHON; delete process.env.EXAMPLE_API_HOST; delete process.env.MINIMAX_API_HOST; delete process.env.HTTP_PROXY; @@ -241,6 +243,7 @@ describe("loadDotEnv", () => { expect(process.env.OPENCLAW_STATE_DIR).toBe(stateDir); expect(process.env.OPENCLAW_CONFIG_PATH).toBeUndefined(); expect(process.env.ANTHROPIC_BASE_URL).toBeUndefined(); + expect(process.env.CLOUDSDK_PYTHON).toBeUndefined(); expect(process.env.EXAMPLE_API_HOST).toBeUndefined(); expect(process.env.MINIMAX_API_HOST).toBeUndefined(); expect(process.env.HTTP_PROXY).toBeUndefined(); diff --git a/src/infra/dotenv.ts b/src/infra/dotenv.ts index aaf1facc402..34e7eb977b9 100644 --- a/src/infra/dotenv.ts +++ b/src/infra/dotenv.ts @@ -22,6 +22,7 @@ const BLOCKED_WORKSPACE_DOTENV_KEYS = new Set([ "CLAWHUB_CONFIG_PATH", "CLAWHUB_TOKEN", "CLAWHUB_URL", + "CLOUDSDK_PYTHON", "HTTP_PROXY", "HTTPS_PROXY", "HOMEBREW_BREW_FILE",