fix: allow safe exec secret passEnv inheritance

This commit is contained in:
Peter Steinberger
2026-05-08 00:00:29 +01:00
parent 3adce8fac1
commit 97d2d40fb7
3 changed files with 96 additions and 1 deletions

View File

@@ -157,6 +157,7 @@ Docs: https://docs.openclaw.ai
- 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.
- 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.
- Doctor/secrets: allow safe inherited exec SecretRef `passEnv` names such as `HOME` while still blocking dangerous runtime env hooks. Fixes #78216.
- Chat commands: make `/model default` reset the session model override instead of treating it as a literal model name. Fixes #78182.
- Cron: make rejected `payload.model` errors show the configured `agents.defaults.models` allowlist instead of echoing the rejected model twice. Fixes #79058.
- 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.

View File

@@ -347,6 +347,89 @@ describe("buildGatewayInstallPlan", () => {
expect(plan.environment.OPENCLAW_SERVICE_MANAGED_ENV_KEYS).toBeUndefined();
});
it("allows safe inherited passEnv names while blocking dangerous exec SecretRef env", async () => {
mockNodeGatewayPlanFixture({
serviceEnvironment: {
OPENCLAW_PORT: "3000",
},
});
const warn = vi.fn();
const plan = await buildGatewayInstallPlan({
env: isolatedPlanEnv({
BASH_ENV: "/tmp/openclaw-test-bashenv",
XDG_CONFIG_HOME: "/tmp/openclaw-test-xdg-home",
XDG_CONFIG_DIRS: "/etc/xdg:/opt/xdg",
GH_TOKEN: "gh-test-token",
AWS_ACCESS_KEY_ID: "aws-access-key",
DOCKER_HOST: "tcp://docker.example.test:2376",
NODE_TLS_REJECT_UNAUTHORIZED: "0",
}),
port: 3000,
runtime: "node",
warn,
config: {
secrets: {
providers: {
onepassword: {
source: "exec",
command: "/usr/bin/op",
args: ["read", "op://Private/Discord/password"],
passEnv: [
"HOME",
"BASH_ENV",
"XDG_CONFIG_HOME",
"XDG_CONFIG_DIRS",
"GH_TOKEN",
"AWS_ACCESS_KEY_ID",
"DOCKER_HOST",
"NODE_TLS_REJECT_UNAUTHORIZED",
],
allowInsecurePath: true,
},
},
},
channels: {
discord: {
token: { source: "exec", provider: "onepassword", id: "value" },
},
},
},
});
expect(plan.environment.HOME).toBe(isolatedHome);
expect(plan.environment.BASH_ENV).toBeUndefined();
expect(plan.environment.XDG_CONFIG_HOME).toBeUndefined();
expect(plan.environment.XDG_CONFIG_DIRS).toBeUndefined();
expect(plan.environment.GH_TOKEN).toBeUndefined();
expect(plan.environment.AWS_ACCESS_KEY_ID).toBeUndefined();
expect(plan.environment.DOCKER_HOST).toBeUndefined();
expect(plan.environment.NODE_TLS_REJECT_UNAUTHORIZED).toBeUndefined();
expect(warn).not.toHaveBeenCalledWith(
'Exec SecretRef passEnv ref "HOME" blocked by host-env security policy',
"Config SecretRef",
);
expect(warn).toHaveBeenCalledWith(
expect.stringContaining("XDG_CONFIG_HOME"),
"Config SecretRef",
);
expect(warn).toHaveBeenCalledWith(
expect.stringContaining("XDG_CONFIG_DIRS"),
"Config SecretRef",
);
expect(warn).toHaveBeenCalledWith(expect.stringContaining("BASH_ENV"), "Config SecretRef");
expect(warn).toHaveBeenCalledWith(expect.stringContaining("GH_TOKEN"), "Config SecretRef");
expect(warn).toHaveBeenCalledWith(
expect.stringContaining("AWS_ACCESS_KEY_ID"),
"Config SecretRef",
);
expect(warn).toHaveBeenCalledWith(expect.stringContaining("DOCKER_HOST"), "Config SecretRef");
expect(warn).toHaveBeenCalledWith(
expect.stringContaining("NODE_TLS_REJECT_UNAUTHORIZED"),
"Config SecretRef",
);
});
it("does not include passEnv values for unused exec SecretRef providers", async () => {
mockNodeGatewayPlanFixture({
serviceEnvironment: {

View File

@@ -60,6 +60,17 @@ const NON_PERSISTED_CONFIG_SECRET_ENV_TARGET_IDS = new Set([
"gateway.auth.password",
"gateway.auth.token",
]);
const EXEC_SECRET_REF_PASS_ENV_ALLOWED_OVERRIDE_ONLY_KEYS = new Set(["HOME"]);
function isBlockedExecSecretRefPassEnvKey(key: string): boolean {
if (isDangerousHostEnvVarName(key)) {
return true;
}
if (!isDangerousHostEnvOverrideVarName(key)) {
return false;
}
return !EXEC_SECRET_REF_PASS_ENV_ALLOWED_OVERRIDE_ONLY_KEYS.has(key.toUpperCase());
}
function loadDaemonInstallAuthProfileSourceRuntime() {
daemonInstallAuthProfileSourceRuntimePromise ??=
@@ -212,7 +223,7 @@ function collectExecSecretRefPassEnvServiceEnvVars(params: {
);
continue;
}
if (isDangerousHostEnvVarName(key) || isDangerousHostEnvOverrideVarName(key)) {
if (isBlockedExecSecretRefPassEnvKey(key)) {
params.warn?.(
`Exec SecretRef passEnv ref "${key}" blocked by host-env security policy`,
"Config SecretRef",