Skip to content

Commit 97d2d40

Browse files
committed
fix: allow safe exec secret passEnv inheritance
1 parent 3adce8f commit 97d2d40

3 files changed

Lines changed: 96 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ Docs: https://docs.openclaw.ai
157157

158158
- Providers: preserve non-OK `text/event-stream` response bodies so provider HTTP errors keep their JSON detail instead of collapsing to generic streaming failures. Fixes #78180.
159159
- Tools/session status: render the active heartbeat/run model for `session_status({"sessionKey":"current"})` instead of falling back to the persisted session default. Fixes #77493.
160+
- Doctor/secrets: allow safe inherited exec SecretRef `passEnv` names such as `HOME` while still blocking dangerous runtime env hooks. Fixes #78216.
160161
- Chat commands: make `/model default` reset the session model override instead of treating it as a literal model name. Fixes #78182.
161162
- Cron: make rejected `payload.model` errors show the configured `agents.defaults.models` allowlist instead of echoing the rejected model twice. Fixes #79058.
162163
- Agents/subagents: retry parent wake announces when the announce-summary model run fails with fallback cooldown exhaustion instead of dropping the wake on the first transient provider overload. Refs #78581.

src/commands/daemon-install-helpers.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,89 @@ describe("buildGatewayInstallPlan", () => {
347347
expect(plan.environment.OPENCLAW_SERVICE_MANAGED_ENV_KEYS).toBeUndefined();
348348
});
349349

350+
it("allows safe inherited passEnv names while blocking dangerous exec SecretRef env", async () => {
351+
mockNodeGatewayPlanFixture({
352+
serviceEnvironment: {
353+
OPENCLAW_PORT: "3000",
354+
},
355+
});
356+
357+
const warn = vi.fn();
358+
const plan = await buildGatewayInstallPlan({
359+
env: isolatedPlanEnv({
360+
BASH_ENV: "/tmp/openclaw-test-bashenv",
361+
XDG_CONFIG_HOME: "/tmp/openclaw-test-xdg-home",
362+
XDG_CONFIG_DIRS: "/etc/xdg:/opt/xdg",
363+
GH_TOKEN: "gh-test-token",
364+
AWS_ACCESS_KEY_ID: "aws-access-key",
365+
DOCKER_HOST: "tcp://docker.example.test:2376",
366+
NODE_TLS_REJECT_UNAUTHORIZED: "0",
367+
}),
368+
port: 3000,
369+
runtime: "node",
370+
warn,
371+
config: {
372+
secrets: {
373+
providers: {
374+
onepassword: {
375+
source: "exec",
376+
command: "/usr/bin/op",
377+
args: ["read", "op://Private/Discord/password"],
378+
passEnv: [
379+
"HOME",
380+
"BASH_ENV",
381+
"XDG_CONFIG_HOME",
382+
"XDG_CONFIG_DIRS",
383+
"GH_TOKEN",
384+
"AWS_ACCESS_KEY_ID",
385+
"DOCKER_HOST",
386+
"NODE_TLS_REJECT_UNAUTHORIZED",
387+
],
388+
allowInsecurePath: true,
389+
},
390+
},
391+
},
392+
channels: {
393+
discord: {
394+
token: { source: "exec", provider: "onepassword", id: "value" },
395+
},
396+
},
397+
},
398+
});
399+
400+
expect(plan.environment.HOME).toBe(isolatedHome);
401+
expect(plan.environment.BASH_ENV).toBeUndefined();
402+
expect(plan.environment.XDG_CONFIG_HOME).toBeUndefined();
403+
expect(plan.environment.XDG_CONFIG_DIRS).toBeUndefined();
404+
expect(plan.environment.GH_TOKEN).toBeUndefined();
405+
expect(plan.environment.AWS_ACCESS_KEY_ID).toBeUndefined();
406+
expect(plan.environment.DOCKER_HOST).toBeUndefined();
407+
expect(plan.environment.NODE_TLS_REJECT_UNAUTHORIZED).toBeUndefined();
408+
expect(warn).not.toHaveBeenCalledWith(
409+
'Exec SecretRef passEnv ref "HOME" blocked by host-env security policy',
410+
"Config SecretRef",
411+
);
412+
expect(warn).toHaveBeenCalledWith(
413+
expect.stringContaining("XDG_CONFIG_HOME"),
414+
"Config SecretRef",
415+
);
416+
expect(warn).toHaveBeenCalledWith(
417+
expect.stringContaining("XDG_CONFIG_DIRS"),
418+
"Config SecretRef",
419+
);
420+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("BASH_ENV"), "Config SecretRef");
421+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("GH_TOKEN"), "Config SecretRef");
422+
expect(warn).toHaveBeenCalledWith(
423+
expect.stringContaining("AWS_ACCESS_KEY_ID"),
424+
"Config SecretRef",
425+
);
426+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("DOCKER_HOST"), "Config SecretRef");
427+
expect(warn).toHaveBeenCalledWith(
428+
expect.stringContaining("NODE_TLS_REJECT_UNAUTHORIZED"),
429+
"Config SecretRef",
430+
);
431+
});
432+
350433
it("does not include passEnv values for unused exec SecretRef providers", async () => {
351434
mockNodeGatewayPlanFixture({
352435
serviceEnvironment: {

src/commands/daemon-install-helpers.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,17 @@ const NON_PERSISTED_CONFIG_SECRET_ENV_TARGET_IDS = new Set([
6060
"gateway.auth.password",
6161
"gateway.auth.token",
6262
]);
63+
const EXEC_SECRET_REF_PASS_ENV_ALLOWED_OVERRIDE_ONLY_KEYS = new Set(["HOME"]);
64+
65+
function isBlockedExecSecretRefPassEnvKey(key: string): boolean {
66+
if (isDangerousHostEnvVarName(key)) {
67+
return true;
68+
}
69+
if (!isDangerousHostEnvOverrideVarName(key)) {
70+
return false;
71+
}
72+
return !EXEC_SECRET_REF_PASS_ENV_ALLOWED_OVERRIDE_ONLY_KEYS.has(key.toUpperCase());
73+
}
6374

6475
function loadDaemonInstallAuthProfileSourceRuntime() {
6576
daemonInstallAuthProfileSourceRuntimePromise ??=
@@ -212,7 +223,7 @@ function collectExecSecretRefPassEnvServiceEnvVars(params: {
212223
);
213224
continue;
214225
}
215-
if (isDangerousHostEnvVarName(key) || isDangerousHostEnvOverrideVarName(key)) {
226+
if (isBlockedExecSecretRefPassEnvKey(key)) {
216227
params.warn?.(
217228
`Exec SecretRef passEnv ref "${key}" blocked by host-env security policy`,
218229
"Config SecretRef",

0 commit comments

Comments
 (0)