Skip to content

Commit 3a189a8

Browse files
committed
fix: block cloud sdk workspace env controls
1 parent 1bcc4c5 commit 3a189a8

6 files changed

Lines changed: 46 additions & 9 deletions

File tree

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ describe("runGcloud", () => {
8484
await withEnvAsync(
8585
{
8686
CLOUDSDK_PYTHON: path.join(tmp, "evil", "python"),
87+
CLOUDSDK_PYTHON_ARGS: "-cprint('attacker')",
8788
PATH: `${shimDir}${path.delimiter}/usr/bin`,
8889
},
8990
async () => {
@@ -109,7 +110,7 @@ describe("runGcloud", () => {
109110
["gcloud", "config", "list"],
110111
{
111112
timeoutMs: 120_000,
112-
env: { CLOUDSDK_PYTHON: realPython },
113+
env: { CLOUDSDK_PYTHON: realPython, CLOUDSDK_PYTHON_ARGS: undefined },
113114
},
114115
);
115116
},
@@ -125,6 +126,7 @@ describe("runGcloud", () => {
125126
await withEnvAsync(
126127
{
127128
CLOUDSDK_PYTHON: "/tmp/attacker-python",
129+
CLOUDSDK_PYTHON_ARGS: "-cprint('attacker')",
128130
PATH: "",
129131
},
130132
async () => {
@@ -141,7 +143,7 @@ describe("runGcloud", () => {
141143
expect(runCommandWithTimeoutMock).toHaveBeenCalledTimes(1);
142144
expect(runCommandWithTimeoutMock).toHaveBeenCalledWith(["gcloud", "config", "list"], {
143145
timeoutMs: 120_000,
144-
env: { CLOUDSDK_PYTHON: undefined },
146+
env: { CLOUDSDK_PYTHON: undefined, CLOUDSDK_PYTHON_ARGS: undefined },
145147
});
146148
},
147149
);

src/hooks/gmail-setup-utils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,9 @@ export async function resolvePythonExecutablePath(): Promise<string | undefined>
151151

152152
async function gcloudEnv(): Promise<NodeJS.ProcessEnv> {
153153
const pythonPath = await resolvePythonExecutablePath();
154-
// Always override inherited CLOUDSDK_PYTHON so gcloud cannot select a
155-
// workspace-controlled interpreter.
156-
return { CLOUDSDK_PYTHON: pythonPath };
154+
// Always override inherited gcloud Python controls so the launcher cannot
155+
// select a workspace-controlled interpreter or word-split injected args.
156+
return { CLOUDSDK_PYTHON: pythonPath, CLOUDSDK_PYTHON_ARGS: undefined };
157157
}
158158

159159
async function runGcloudCommand(

src/infra/dotenv.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,10 @@ describe("loadDotEnv", () => {
299299
"OPENCLAW_STATE_DIR=./evil-state",
300300
"OPENCLAW_CONFIG_PATH=./evil-config.json",
301301
"ANTHROPIC_BASE_URL=https://evil.example.com/v1",
302+
"CLOUDSDK_CONFIG=./attacker-gcloud-config",
302303
"CLOUDSDK_PYTHON=./attacker-python",
304+
"CLOUDSDK_PYTHON_ARGS=-cprint('attacker')",
305+
"CLOUDSDK_PYTHON_SITEPACKAGES=1",
303306
"EXAMPLE_API_HOST=https://evil-api.example.com",
304307
"MINIMAX_API_HOST=https://evil.example.com",
305308
"SLACK_API_URL=http://evil-slack.example.com/api/",
@@ -324,7 +327,10 @@ describe("loadDotEnv", () => {
324327
delete process.env.NODE_V8_COVERAGE;
325328
deleteTestEnvValue("OPENCLAW_CONFIG_PATH");
326329
delete process.env.ANTHROPIC_BASE_URL;
330+
delete process.env.CLOUDSDK_CONFIG;
327331
delete process.env.CLOUDSDK_PYTHON;
332+
delete process.env.CLOUDSDK_PYTHON_ARGS;
333+
delete process.env.CLOUDSDK_PYTHON_SITEPACKAGES;
328334
delete process.env.EXAMPLE_API_HOST;
329335
delete process.env.MINIMAX_API_HOST;
330336
delete process.env.SLACK_API_URL;
@@ -349,7 +355,10 @@ describe("loadDotEnv", () => {
349355
expect(process.env.OPENCLAW_STATE_DIR).toBe(stateDir);
350356
expect(process.env.OPENCLAW_CONFIG_PATH).toBeUndefined();
351357
expect(process.env.ANTHROPIC_BASE_URL).toBeUndefined();
358+
expect(process.env.CLOUDSDK_CONFIG).toBeUndefined();
352359
expect(process.env.CLOUDSDK_PYTHON).toBeUndefined();
360+
expect(process.env.CLOUDSDK_PYTHON_ARGS).toBeUndefined();
361+
expect(process.env.CLOUDSDK_PYTHON_SITEPACKAGES).toBeUndefined();
353362
expect(process.env.EXAMPLE_API_HOST).toBeUndefined();
354363
expect(process.env.MINIMAX_API_HOST).toBeUndefined();
355364
expect(process.env.SLACK_API_URL).toBeUndefined();
@@ -973,6 +982,10 @@ describe("workspace .env blocklist completeness", () => {
973982
"MATRIX_HOMESERVER",
974983
"MINIMAX_API_HOST",
975984
"BROWSER_EXECUTABLE_PATH",
985+
"CLOUDSDK_CONFIG",
986+
"CLOUDSDK_PYTHON",
987+
"CLOUDSDK_PYTHON_ARGS",
988+
"CLOUDSDK_PYTHON_SITEPACKAGES",
976989
"PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH",
977990
"OPENCLAW_SKIP_CHANNELS",
978991
"OPENCLAW_SKIP_PROVIDERS",

src/infra/dotenv.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,6 @@ const BLOCKED_WORKSPACE_DOTENV_KEYS = new Set([
106106
"CLAWHUB_CONFIG_PATH",
107107
"CLAWHUB_TOKEN",
108108
"CLAWHUB_URL",
109-
"CLOUDSDK_PYTHON",
110109
"COMSPEC",
111110
"HTTP_PROXY",
112111
"HTTPS_PROXY",
@@ -184,6 +183,9 @@ const BLOCKED_WORKSPACE_DOTENV_SUFFIXES = ["_API_HOST", "_BASE_URL", "_HOMESERVE
184183
const BLOCKED_WORKSPACE_DOTENV_PREFIXES = [
185184
"ANTHROPIC_API_KEY_",
186185
"CLAWHUB_",
186+
// Google Cloud SDK launchers treat CLOUDSDK_* values as runtime controls.
187+
// Workspace .env must not steer gcloud subprocess interpreters or args.
188+
"CLOUDSDK_",
187189
"OPENAI_API_KEY_",
188190
// Workspace .env is untrusted; reserve the full OpenClaw runtime namespace
189191
// for shell/global config so new OPENCLAW_* controls are fail-closed by default.

src/process/exec.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,14 +120,17 @@ describe("runCommandWithTimeout", () => {
120120
argv: ["node", "script.js"],
121121
baseEnv: {
122122
OPENCLAW_BASE_ENV: "base",
123+
OPENCLAW_CHILD_ENV_REMOVE: "base",
123124
OPENCLAW_TO_REMOVE: undefined,
124125
},
125126
env: {
127+
OPENCLAW_CHILD_ENV_REMOVE: undefined,
126128
OPENCLAW_TEST_ENV: "ok",
127129
},
128130
});
129131

130132
expect(resolved.OPENCLAW_BASE_ENV).toBe("base");
133+
expect(resolved.OPENCLAW_CHILD_ENV_REMOVE).toBeUndefined();
131134
expect(resolved.OPENCLAW_TEST_ENV).toBe("ok");
132135
expect(resolved.OPENCLAW_TO_REMOVE).toBeUndefined();
133136
expect(resolved.OPENCLAW_CLI).toBe(OPENCLAW_CLI_ENV_VALUE);
@@ -153,6 +156,22 @@ describe("runCommandWithTimeout", () => {
153156
expect(resolved.OPENCLAW_TEST_ENV).toBe("ok");
154157
});
155158

159+
it("removes case-insensitive inherited env keys on Windows", () => {
160+
const resolved = resolveCommandEnv({
161+
argv: ["node", "script.js"],
162+
platform: "win32",
163+
baseEnv: {
164+
Path: "C:\\base\\bin",
165+
},
166+
env: {
167+
PATH: undefined,
168+
},
169+
});
170+
171+
expect(resolved.Path).toBeUndefined();
172+
expect(resolved.PATH).toBeUndefined();
173+
});
174+
156175
it("preserves case-distinct env keys outside Windows", () => {
157176
const resolved = resolveCommandEnv({
158177
argv: ["node", "script.js"],

src/process/exec.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,6 @@ function assignChildEnvValue(params: {
3333
platform: NodeJS.Platform;
3434
value: string | undefined;
3535
}): void {
36-
if (params.value === undefined) {
37-
return;
38-
}
3936
if (params.platform === "win32") {
4037
const normalizedKey = params.key.toLowerCase();
4138
for (const existingKey of Object.keys(params.env)) {
@@ -44,6 +41,10 @@ function assignChildEnvValue(params: {
4441
}
4542
}
4643
}
44+
if (params.value === undefined) {
45+
delete params.env[params.key];
46+
return;
47+
}
4748
params.env[params.key] = params.value;
4849
}
4950

0 commit comments

Comments
 (0)