fix: block workspace CLOUDSDK_PYTHON override and always set trusted interpreter for gcloud (#74492)

* fix: address issue

* docs: add changelog entry for PR merge
This commit is contained in:
Pavan Kumar Gondhi
2026-05-01 18:35:03 +05:30
committed by GitHub
parent cba0a348dc
commit 86251f4391
5 changed files with 92 additions and 7 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -145,14 +145,10 @@ export async function resolvePythonExecutablePath(): Promise<string | undefined>
return undefined;
}
async function gcloudEnv(): Promise<NodeJS.ProcessEnv | undefined> {
if (process.env.CLOUDSDK_PYTHON) {
return undefined;
}
async function gcloudEnv(): Promise<NodeJS.ProcessEnv> {
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 };
}

View File

@@ -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();

View File

@@ -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",