mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-16 12:10:45 +00:00
fix: allow safe exec secret passEnv inheritance
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user