fix(sandbox): sanitize Docker env before marking OPENCLAW_CLI (#42256)

* Sandbox: sanitize Docker env before exec marker injection

* Sandbox: add regression test for Docker exec marker env

* Sandbox: disable Windows shell fallback for Docker

* Sandbox: cover Windows Docker wrapper rejection

* Sandbox: test strict env sanitization through Docker args
This commit is contained in:
Vincent Koc
2026-03-11 00:59:36 -04:00
committed by GitHub
parent 061b8258bc
commit bd33a340fb
3 changed files with 41 additions and 14 deletions

View File

@@ -137,6 +137,33 @@ describe("buildSandboxCreateArgs", () => {
); );
}); });
it("preserves the OpenClaw exec marker when strict env sanitization is enabled", () => {
const cfg = createSandboxConfig({
env: {
NODE_ENV: "test",
},
});
const args = buildSandboxCreateArgs({
name: "openclaw-sbx-marker",
cfg,
scopeKey: "main",
createdAtMs: 1700000000000,
envSanitizationOptions: {
strictMode: true,
},
});
expect(args).toEqual(
expect.arrayContaining([
"--env",
"NODE_ENV=test",
"--env",
`OPENCLAW_CLI=${OPENCLAW_CLI_ENV_VALUE}`,
]),
);
});
it("emits -v flags for safe custom binds", () => { it("emits -v flags for safe custom binds", () => {
const cfg: SandboxDockerConfig = { const cfg: SandboxDockerConfig = {
image: "openclaw-sandbox:bookworm-slim", image: "openclaw-sandbox:bookworm-slim",

View File

@@ -5,6 +5,7 @@ import {
resolveWindowsSpawnProgram, resolveWindowsSpawnProgram,
} from "../../plugin-sdk/windows-spawn.js"; } from "../../plugin-sdk/windows-spawn.js";
import { sanitizeEnvVars } from "./sanitize-env-vars.js"; import { sanitizeEnvVars } from "./sanitize-env-vars.js";
import type { EnvSanitizationOptions } from "./sanitize-env-vars.js";
type ExecDockerRawOptions = { type ExecDockerRawOptions = {
allowFailure?: boolean; allowFailure?: boolean;
@@ -52,7 +53,7 @@ export function resolveDockerSpawnInvocation(
env: runtime.env, env: runtime.env,
execPath: runtime.execPath, execPath: runtime.execPath,
packageName: "docker", packageName: "docker",
allowShellFallback: true, allowShellFallback: false,
}); });
const resolved = materializeWindowsSpawnProgram(program, args); const resolved = materializeWindowsSpawnProgram(program, args);
return { return {
@@ -325,6 +326,7 @@ export function buildSandboxCreateArgs(params: {
allowSourcesOutsideAllowedRoots?: boolean; allowSourcesOutsideAllowedRoots?: boolean;
allowReservedContainerTargets?: boolean; allowReservedContainerTargets?: boolean;
allowContainerNamespaceJoin?: boolean; allowContainerNamespaceJoin?: boolean;
envSanitizationOptions?: EnvSanitizationOptions;
}) { }) {
// Runtime security validation: blocks dangerous bind mounts, network modes, and profiles. // Runtime security validation: blocks dangerous bind mounts, network modes, and profiles.
validateSandboxSecurity({ validateSandboxSecurity({
@@ -366,14 +368,14 @@ export function buildSandboxCreateArgs(params: {
if (params.cfg.user) { if (params.cfg.user) {
args.push("--user", params.cfg.user); args.push("--user", params.cfg.user);
} }
const envSanitization = sanitizeEnvVars(markOpenClawExecEnv(params.cfg.env ?? {})); const envSanitization = sanitizeEnvVars(params.cfg.env ?? {}, params.envSanitizationOptions);
if (envSanitization.blocked.length > 0) { if (envSanitization.blocked.length > 0) {
log.warn(`Blocked sensitive environment variables: ${envSanitization.blocked.join(", ")}`); log.warn(`Blocked sensitive environment variables: ${envSanitization.blocked.join(", ")}`);
} }
if (envSanitization.warnings.length > 0) { if (envSanitization.warnings.length > 0) {
log.warn(`Suspicious environment variables: ${envSanitization.warnings.join(", ")}`); log.warn(`Suspicious environment variables: ${envSanitization.warnings.join(", ")}`);
} }
for (const [key, value] of Object.entries(envSanitization.allowed)) { for (const [key, value] of Object.entries(markOpenClawExecEnv(envSanitization.allowed))) {
args.push("--env", `${key}=${value}`); args.push("--env", `${key}=${value}`);
} }
for (const cap of params.cfg.capDrop) { for (const cap of params.cfg.capDrop) {

View File

@@ -47,22 +47,20 @@ describe("resolveDockerSpawnInvocation", () => {
}); });
}); });
it("falls back to shell mode when only unresolved docker.cmd wrapper exists", async () => { it("rejects unresolved docker.cmd wrappers instead of shelling out", async () => {
const dir = await createTempDir(); const dir = await createTempDir();
const cmdPath = path.join(dir, "docker.cmd"); const cmdPath = path.join(dir, "docker.cmd");
await mkdir(path.dirname(cmdPath), { recursive: true }); await mkdir(path.dirname(cmdPath), { recursive: true });
await writeFile(cmdPath, "@ECHO off\r\necho docker\r\n", "utf8"); await writeFile(cmdPath, "@ECHO off\r\necho docker\r\n", "utf8");
const resolved = resolveDockerSpawnInvocation(["ps"], { expect(() =>
platform: "win32", resolveDockerSpawnInvocation(["ps"], {
env: { PATH: dir, PATHEXT: ".CMD;.EXE;.BAT" }, platform: "win32",
execPath: "C:\\node\\node.exe", env: { PATH: dir, PATHEXT: ".CMD;.EXE;.BAT" },
}); execPath: "C:\\node\\node.exe",
expect(path.normalize(resolved.command).toLowerCase()).toBe( }),
path.normalize(cmdPath).toLowerCase(), ).toThrow(
/wrapper resolved, but no executable\/Node entrypoint could be resolved without shell execution\./i,
); );
expect(resolved.args).toEqual(["ps"]);
expect(resolved.shell).toBe(true);
expect(resolved.windowsHide).toBeUndefined();
}); });
}); });