From d1bfe084241a141e4cfcd449aa0a42374ae5bbaf Mon Sep 17 00:00:00 2001 From: Jacob Tomlinson Date: Wed, 25 Mar 2026 12:57:22 -0700 Subject: [PATCH] fix: apply host-env blocklist to auth-profile env refs in daemon install (#54627) * fix: apply host-env blocklist to auth-profile env refs in daemon install Co-Authored-By: Claude Opus 4.6 (1M context) * ci: retrigger checks --------- Co-authored-by: Claude Opus 4.6 (1M context) --- src/commands/daemon-install-helpers.test.ts | 74 +++++++++++++++++++++ src/commands/daemon-install-helpers.ts | 24 ++++++- 2 files changed, 96 insertions(+), 2 deletions(-) diff --git a/src/commands/daemon-install-helpers.test.ts b/src/commands/daemon-install-helpers.test.ts index 8adc0aa5a9f..61c5e2f6e50 100644 --- a/src/commands/daemon-install-helpers.test.ts +++ b/src/commands/daemon-install-helpers.test.ts @@ -315,6 +315,80 @@ describe("buildGatewayInstallPlan", () => { expect(plan.environment.ANTHROPIC_TOKEN).toBe("ant-test-token"); }); + it("blocks dangerous auth-profile env refs from the service environment", async () => { + mockNodeGatewayPlanFixture({ + serviceEnvironment: { + OPENCLAW_PORT: "3000", + }, + }); + mocks.loadAuthProfileStoreForSecretsRuntime.mockReturnValue({ + version: 1, + profiles: { + "node:default": { + type: "token", + provider: "node", + tokenRef: { source: "env", provider: "default", id: "NODE_OPTIONS" }, + }, + "git:default": { + type: "token", + provider: "git", + tokenRef: { source: "env", provider: "default", id: "GIT_ASKPASS" }, + }, + "openai:default": { + type: "api_key", + provider: "openai", + keyRef: { source: "env", provider: "default", id: "OPENAI_API_KEY" }, + }, + }, + }); + + const warn = vi.fn(); + const plan = await buildGatewayInstallPlan({ + env: { + NODE_OPTIONS: "--require ./pwn.js", + GIT_ASKPASS: "/tmp/askpass.sh", + OPENAI_API_KEY: "sk-openai-test", // pragma: allowlist secret + }, + port: 3000, + runtime: "node", + warn, + }); + + expect(plan.environment.NODE_OPTIONS).toBeUndefined(); + expect(plan.environment.GIT_ASKPASS).toBeUndefined(); + expect(plan.environment.OPENAI_API_KEY).toBe("sk-openai-test"); + expect(warn).toHaveBeenCalledWith(expect.stringContaining("NODE_OPTIONS"), "Auth profile"); + expect(warn).toHaveBeenCalledWith(expect.stringContaining("GIT_ASKPASS"), "Auth profile"); + }); + + it("skips non-portable auth-profile env ref keys", async () => { + mockNodeGatewayPlanFixture({ + serviceEnvironment: { + OPENCLAW_PORT: "3000", + }, + }); + mocks.loadAuthProfileStoreForSecretsRuntime.mockReturnValue({ + version: 1, + profiles: { + "broken:default": { + type: "token", + provider: "broken", + tokenRef: { source: "env", provider: "default", id: "BAD KEY" }, + }, + }, + }); + + const plan = await buildGatewayInstallPlan({ + env: { + "BAD KEY": "should-not-pass", + }, + port: 3000, + runtime: "node", + }); + + expect(plan.environment["BAD KEY"]).toBeUndefined(); + }); + it("skips unresolved auth-profile env refs", async () => { mockNodeGatewayPlanFixture({ serviceEnvironment: { diff --git a/src/commands/daemon-install-helpers.ts b/src/commands/daemon-install-helpers.ts index 62d18ea7d0d..ac20612d8eb 100644 --- a/src/commands/daemon-install-helpers.ts +++ b/src/commands/daemon-install-helpers.ts @@ -8,6 +8,11 @@ import type { OpenClawConfig } from "../config/types.js"; import { resolveGatewayLaunchAgentLabel } from "../daemon/constants.js"; import { resolveGatewayProgramArguments } from "../daemon/program-args.js"; import { buildServiceEnvironment } from "../daemon/service-env.js"; +import { + isDangerousHostEnvOverrideVarName, + isDangerousHostEnvVarName, + normalizeEnvVarKey, +} from "../infra/host-env-security.js"; import { emitDaemonInstallRuntimeWarning, resolveDaemonInstallRuntimeInputs, @@ -27,6 +32,7 @@ export type GatewayInstallPlan = { function collectAuthProfileServiceEnvVars(params: { env: Record; authStore?: AuthProfileStore; + warn?: DaemonInstallWarnFn; }): Record { const authStore = params.authStore ?? loadAuthProfileStoreForSecretsRuntime(); const entries: Record = {}; @@ -41,11 +47,22 @@ function collectAuthProfileServiceEnvVars(params: { if (!ref || ref.source !== "env") { continue; } - const value = params.env[ref.id]?.trim(); + const key = normalizeEnvVarKey(ref.id, { portable: true }); + if (!key) { + continue; + } + if (isDangerousHostEnvVarName(key) || isDangerousHostEnvOverrideVarName(key)) { + params.warn?.( + `Auth profile env ref "${key}" blocked by host-env security policy`, + "Auth profile", + ); + continue; + } + const value = params.env[key]?.trim(); if (!value) { continue; } - entries[ref.id] = value; + entries[key] = value; } return entries; @@ -55,6 +72,7 @@ function buildGatewayInstallEnvironment(params: { env: Record; config?: OpenClawConfig; authStore?: AuthProfileStore; + warn?: DaemonInstallWarnFn; serviceEnvironment: Record; }): Record { const environment: Record = { @@ -65,6 +83,7 @@ function buildGatewayInstallEnvironment(params: { ...collectAuthProfileServiceEnvVars({ env: params.env, authStore: params.authStore, + warn: params.warn, }), }; Object.assign(environment, params.serviceEnvironment); @@ -125,6 +144,7 @@ export async function buildGatewayInstallPlan(params: { env: params.env, config: params.config, authStore: params.authStore, + warn: params.warn, serviceEnvironment, }), };