Skip to content

Commit 9238d72

Browse files
committed
fix: address issue
1 parent 240362b commit 9238d72

4 files changed

Lines changed: 91 additions & 7 deletions

File tree

src/hooks/gmail-setup-utils.test.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
ensureTailscaleEndpoint,
88
resetGmailSetupUtilsCachesForTest,
99
resolvePythonExecutablePath,
10+
runGcloud,
1011
} from "./gmail-setup-utils.js";
1112

1213
const itUnix = process.platform === "win32" ? it.skip : it;
@@ -63,6 +64,89 @@ describe("resolvePythonExecutablePath", () => {
6364
);
6465
});
6566

67+
describe("runGcloud", () => {
68+
itUnix(
69+
"overrides an inherited CLOUDSDK_PYTHON value with a resolved interpreter",
70+
async () => {
71+
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gcloud-python-"));
72+
try {
73+
const realPython = path.join(tmp, "python-real");
74+
await fs.writeFile(realPython, "#!/bin/sh\nexit 0\n", "utf-8");
75+
await fs.chmod(realPython, 0o755);
76+
77+
const shimDir = path.join(tmp, "shims");
78+
await fs.mkdir(shimDir, { recursive: true });
79+
const shim = path.join(shimDir, "python3");
80+
await fs.writeFile(shim, "#!/bin/sh\nexit 0\n", "utf-8");
81+
await fs.chmod(shim, 0o755);
82+
83+
await withEnvAsync(
84+
{
85+
CLOUDSDK_PYTHON: path.join(tmp, "evil", "python"),
86+
PATH: `${shimDir}${path.delimiter}/usr/bin`,
87+
},
88+
async () => {
89+
runCommandWithTimeoutMock
90+
.mockResolvedValueOnce({
91+
stdout: `${realPython}\n`,
92+
stderr: "",
93+
code: 0,
94+
signal: null,
95+
killed: false,
96+
})
97+
.mockResolvedValueOnce({
98+
stdout: "",
99+
stderr: "",
100+
code: 0,
101+
signal: null,
102+
killed: false,
103+
});
104+
105+
await runGcloud(["config", "list"]);
106+
107+
expect(runCommandWithTimeoutMock).toHaveBeenLastCalledWith(
108+
["gcloud", "config", "list"],
109+
{
110+
timeoutMs: 120_000,
111+
env: { CLOUDSDK_PYTHON: realPython },
112+
},
113+
);
114+
},
115+
);
116+
} finally {
117+
await fs.rm(tmp, { recursive: true, force: true });
118+
}
119+
},
120+
60_000,
121+
);
122+
123+
itUnix("unsets inherited CLOUDSDK_PYTHON when no trusted interpreter is found", async () => {
124+
await withEnvAsync(
125+
{
126+
CLOUDSDK_PYTHON: "/tmp/attacker-python",
127+
PATH: "",
128+
},
129+
async () => {
130+
runCommandWithTimeoutMock.mockResolvedValueOnce({
131+
stdout: "",
132+
stderr: "",
133+
code: 0,
134+
signal: null,
135+
killed: false,
136+
});
137+
138+
await runGcloud(["config", "list"]);
139+
140+
expect(runCommandWithTimeoutMock).toHaveBeenCalledTimes(1);
141+
expect(runCommandWithTimeoutMock).toHaveBeenCalledWith(["gcloud", "config", "list"], {
142+
timeoutMs: 120_000,
143+
env: { CLOUDSDK_PYTHON: undefined },
144+
});
145+
},
146+
);
147+
});
148+
});
149+
66150
describe("ensureTailscaleEndpoint", () => {
67151
it("includes stdout and exit code when tailscale serve fails", async () => {
68152
runCommandWithTimeoutMock

src/hooks/gmail-setup-utils.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -145,14 +145,10 @@ export async function resolvePythonExecutablePath(): Promise<string | undefined>
145145
return undefined;
146146
}
147147

148-
async function gcloudEnv(): Promise<NodeJS.ProcessEnv | undefined> {
149-
if (process.env.CLOUDSDK_PYTHON) {
150-
return undefined;
151-
}
148+
async function gcloudEnv(): Promise<NodeJS.ProcessEnv> {
152149
const pythonPath = await resolvePythonExecutablePath();
153-
if (!pythonPath) {
154-
return undefined;
155-
}
150+
// Always override inherited CLOUDSDK_PYTHON so gcloud cannot select a
151+
// workspace-controlled interpreter.
156152
return { CLOUDSDK_PYTHON: pythonPath };
157153
}
158154

src/infra/dotenv.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ describe("loadDotEnv", () => {
197197
"OPENCLAW_STATE_DIR=./evil-state",
198198
"OPENCLAW_CONFIG_PATH=./evil-config.json",
199199
"ANTHROPIC_BASE_URL=https://evil.example.com/v1",
200+
"CLOUDSDK_PYTHON=./attacker-python",
200201
"EXAMPLE_API_HOST=https://evil-api.example.com",
201202
"MINIMAX_API_HOST=https://evil.example.com",
202203
"HTTP_PROXY=http://evil-proxy:8080",
@@ -211,6 +212,7 @@ describe("loadDotEnv", () => {
211212
delete process.env.NODE_OPTIONS;
212213
delete process.env.OPENCLAW_CONFIG_PATH;
213214
delete process.env.ANTHROPIC_BASE_URL;
215+
delete process.env.CLOUDSDK_PYTHON;
214216
delete process.env.EXAMPLE_API_HOST;
215217
delete process.env.MINIMAX_API_HOST;
216218
delete process.env.HTTP_PROXY;
@@ -225,6 +227,7 @@ describe("loadDotEnv", () => {
225227
expect(process.env.OPENCLAW_STATE_DIR).toBe(stateDir);
226228
expect(process.env.OPENCLAW_CONFIG_PATH).toBeUndefined();
227229
expect(process.env.ANTHROPIC_BASE_URL).toBeUndefined();
230+
expect(process.env.CLOUDSDK_PYTHON).toBeUndefined();
228231
expect(process.env.EXAMPLE_API_HOST).toBeUndefined();
229232
expect(process.env.MINIMAX_API_HOST).toBeUndefined();
230233
expect(process.env.HTTP_PROXY).toBeUndefined();

src/infra/dotenv.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const BLOCKED_WORKSPACE_DOTENV_KEYS = new Set([
1919
"CLAWHUB_CONFIG_PATH",
2020
"CLAWHUB_TOKEN",
2121
"CLAWHUB_URL",
22+
"CLOUDSDK_PYTHON",
2223
"HTTP_PROXY",
2324
"HTTPS_PROXY",
2425
"IRC_HOST",

0 commit comments

Comments
 (0)