Adjust CLI backend environment handling before spawn (#53921)

security(agents): sanitize CLI backend env overrides before spawn
This commit is contained in:
Devin Robison
2026-03-24 12:58:10 -07:00
committed by GitHub
parent 1beda4aff1
commit c2fb7f1948
2 changed files with 117 additions and 2 deletions

View File

@@ -1,7 +1,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { resolveCliNoOutputTimeoutMs } from "./cli-runner/helpers.js";
import type { EmbeddedContextFile } from "./pi-embedded-helpers.js";
@@ -102,6 +102,10 @@ function createManagedRun(exit: MockRunExit, pid = 1234) {
}
describe("runCliAgent with process supervisor", () => {
afterEach(() => {
vi.unstubAllEnvs();
});
beforeEach(async () => {
supervisorSpawnMock.mockClear();
enqueueSystemEventMock.mockClear();
@@ -157,6 +161,112 @@ describe("runCliAgent with process supervisor", () => {
expect(input.scopeKey).toContain("thread-123");
});
it("sanitizes dangerous backend env overrides before spawn", async () => {
vi.stubEnv("PATH", "/usr/bin:/bin");
vi.stubEnv("HOME", "/tmp/trusted-home");
supervisorSpawnMock.mockResolvedValueOnce(
createManagedRun({
reason: "exit",
exitCode: 0,
exitSignal: null,
durationMs: 50,
stdout: "ok",
stderr: "",
timedOut: false,
noOutputTimedOut: false,
}),
);
await runCliAgent({
sessionId: "s1",
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp",
config: {
agents: {
defaults: {
cliBackends: {
"codex-cli": {
env: {
NODE_OPTIONS: "--require ./malicious.js",
LD_PRELOAD: "/tmp/pwn.so",
PATH: "/tmp/evil",
HOME: "/tmp/evil-home",
SAFE_KEY: "ok",
},
},
},
},
},
} satisfies OpenClawConfig,
prompt: "hi",
provider: "codex-cli",
model: "gpt-5.2-codex",
timeoutMs: 1_000,
runId: "run-env-sanitized",
cliSessionId: "thread-123",
});
const input = supervisorSpawnMock.mock.calls[0]?.[0] as {
env?: Record<string, string | undefined>;
};
expect(input.env?.SAFE_KEY).toBe("ok");
expect(input.env?.PATH).toBe("/usr/bin:/bin");
expect(input.env?.HOME).toBe("/tmp/trusted-home");
expect(input.env?.NODE_OPTIONS).toBeUndefined();
expect(input.env?.LD_PRELOAD).toBeUndefined();
});
it("applies clearEnv after sanitizing backend env overrides", async () => {
vi.stubEnv("PATH", "/usr/bin:/bin");
vi.stubEnv("SAFE_CLEAR", "from-base");
supervisorSpawnMock.mockResolvedValueOnce(
createManagedRun({
reason: "exit",
exitCode: 0,
exitSignal: null,
durationMs: 50,
stdout: "ok",
stderr: "",
timedOut: false,
noOutputTimedOut: false,
}),
);
await runCliAgent({
sessionId: "s1",
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp",
config: {
agents: {
defaults: {
cliBackends: {
"codex-cli": {
env: {
SAFE_KEEP: "keep-me",
},
clearEnv: ["SAFE_CLEAR"],
},
},
},
},
} satisfies OpenClawConfig,
prompt: "hi",
provider: "codex-cli",
model: "gpt-5.2-codex",
timeoutMs: 1_000,
runId: "run-clear-env",
cliSessionId: "thread-123",
});
const input = supervisorSpawnMock.mock.calls[0]?.[0] as {
env?: Record<string, string | undefined>;
};
expect(input.env?.SAFE_KEEP).toBe("keep-me");
expect(input.env?.SAFE_CLEAR).toBeUndefined();
});
it("prepends bootstrap warnings to the CLI prompt body", async () => {
supervisorSpawnMock.mockResolvedValueOnce(
createManagedRun({

View File

@@ -5,6 +5,7 @@ import type { OpenClawConfig } from "../config/config.js";
import { shouldLogVerbose } from "../globals.js";
import { isTruthyEnvValue } from "../infra/env.js";
import { requestHeartbeatNow } from "../infra/heartbeat-wake.js";
import { sanitizeHostExecEnv } from "../infra/host-env-security.js";
import { enqueueSystemEvent } from "../infra/system-events.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { getProcessSupervisor } from "../process/supervisor/index.js";
@@ -296,7 +297,11 @@ export async function runCliAgent(params: {
}
const env = (() => {
const next = { ...process.env, ...backend.env };
const next = sanitizeHostExecEnv({
baseEnv: process.env,
overrides: backend.env,
blockPathOverrides: true,
});
for (const key of backend.clearEnv ?? []) {
delete next[key];
}