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", () => {
const cfg: SandboxDockerConfig = {
image: "openclaw-sandbox:bookworm-slim",

View File

@@ -5,6 +5,7 @@ import {
resolveWindowsSpawnProgram,
} from "../../plugin-sdk/windows-spawn.js";
import { sanitizeEnvVars } from "./sanitize-env-vars.js";
import type { EnvSanitizationOptions } from "./sanitize-env-vars.js";
type ExecDockerRawOptions = {
allowFailure?: boolean;
@@ -52,7 +53,7 @@ export function resolveDockerSpawnInvocation(
env: runtime.env,
execPath: runtime.execPath,
packageName: "docker",
allowShellFallback: true,
allowShellFallback: false,
});
const resolved = materializeWindowsSpawnProgram(program, args);
return {
@@ -325,6 +326,7 @@ export function buildSandboxCreateArgs(params: {
allowSourcesOutsideAllowedRoots?: boolean;
allowReservedContainerTargets?: boolean;
allowContainerNamespaceJoin?: boolean;
envSanitizationOptions?: EnvSanitizationOptions;
}) {
// Runtime security validation: blocks dangerous bind mounts, network modes, and profiles.
validateSandboxSecurity({
@@ -366,14 +368,14 @@ export function buildSandboxCreateArgs(params: {
if (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) {
log.warn(`Blocked sensitive environment variables: ${envSanitization.blocked.join(", ")}`);
}
if (envSanitization.warnings.length > 0) {
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}`);
}
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 cmdPath = path.join(dir, "docker.cmd");
await mkdir(path.dirname(cmdPath), { recursive: true });
await writeFile(cmdPath, "@ECHO off\r\necho docker\r\n", "utf8");
const resolved = resolveDockerSpawnInvocation(["ps"], {
platform: "win32",
env: { PATH: dir, PATHEXT: ".CMD;.EXE;.BAT" },
execPath: "C:\\node\\node.exe",
});
expect(path.normalize(resolved.command).toLowerCase()).toBe(
path.normalize(cmdPath).toLowerCase(),
expect(() =>
resolveDockerSpawnInvocation(["ps"], {
platform: "win32",
env: { PATH: dir, PATHEXT: ".CMD;.EXE;.BAT" },
execPath: "C:\\node\\node.exe",
}),
).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();
});
});