mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-01 20:31:19 +00:00
Exec: harden host env override handling across gateway and node (#51207)
* Exec: harden host env override enforcement and fail closed * Node host: enforce env override diagnostics before shell filtering * Env overrides: align Windows key handling and mac node rejection
This commit is contained in:
@@ -336,6 +336,7 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
|
||||
preferMacAppExecHost: boolean;
|
||||
runViaResponse?: ExecHostResponse | null;
|
||||
command?: string[];
|
||||
env?: Record<string, string>;
|
||||
rawCommand?: string | null;
|
||||
systemRunPlan?: SystemRunApprovalPlan | null;
|
||||
cwd?: string;
|
||||
@@ -391,6 +392,7 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
|
||||
client: {} as never,
|
||||
params: {
|
||||
command: params.command ?? ["echo", "ok"],
|
||||
env: params.env,
|
||||
rawCommand: params.rawCommand,
|
||||
systemRunPlan: params.systemRunPlan,
|
||||
cwd: params.cwd,
|
||||
@@ -1106,6 +1108,65 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
|
||||
expectApprovalRequiredDenied({ sendNodeEvent, sendInvokeResult });
|
||||
});
|
||||
|
||||
it("rejects blocked environment overrides before execution", async () => {
|
||||
const { runCommand, sendInvokeResult } = await runSystemInvoke({
|
||||
preferMacAppExecHost: false,
|
||||
security: "full",
|
||||
ask: "off",
|
||||
env: { CLASSPATH: "/tmp/evil-classpath" },
|
||||
});
|
||||
|
||||
expect(runCommand).not.toHaveBeenCalled();
|
||||
expectInvokeErrorMessage(sendInvokeResult, {
|
||||
message: "SYSTEM_RUN_DENIED: environment override rejected",
|
||||
});
|
||||
expectInvokeErrorMessage(sendInvokeResult, {
|
||||
message: "CLASSPATH",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects blocked environment overrides for shell-wrapper commands", async () => {
|
||||
const shellCommand =
|
||||
process.platform === "win32"
|
||||
? ["cmd.exe", "/d", "/s", "/c", "echo ok"]
|
||||
: ["/bin/sh", "-lc", "echo ok"];
|
||||
const { runCommand, sendInvokeResult } = await runSystemInvoke({
|
||||
preferMacAppExecHost: false,
|
||||
security: "full",
|
||||
ask: "off",
|
||||
command: shellCommand,
|
||||
env: {
|
||||
CLASSPATH: "/tmp/evil-classpath",
|
||||
LANG: "C",
|
||||
},
|
||||
});
|
||||
|
||||
expect(runCommand).not.toHaveBeenCalled();
|
||||
expectInvokeErrorMessage(sendInvokeResult, {
|
||||
message: "SYSTEM_RUN_DENIED: environment override rejected",
|
||||
});
|
||||
expectInvokeErrorMessage(sendInvokeResult, {
|
||||
message: "CLASSPATH",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects invalid non-portable environment override keys before execution", async () => {
|
||||
const { runCommand, sendInvokeResult } = await runSystemInvoke({
|
||||
preferMacAppExecHost: false,
|
||||
security: "full",
|
||||
ask: "off",
|
||||
env: { "BAD-KEY": "x" },
|
||||
});
|
||||
|
||||
expect(runCommand).not.toHaveBeenCalled();
|
||||
expectInvokeErrorMessage(sendInvokeResult, {
|
||||
message: "SYSTEM_RUN_DENIED: environment override rejected",
|
||||
});
|
||||
expectInvokeErrorMessage(sendInvokeResult, {
|
||||
message: "BAD-KEY",
|
||||
});
|
||||
});
|
||||
|
||||
async function expectNestedEnvShellDenied(params: {
|
||||
depth: number;
|
||||
markerName: string;
|
||||
|
||||
@@ -14,7 +14,10 @@ import {
|
||||
} from "../infra/exec-approvals.js";
|
||||
import type { ExecHostRequest, ExecHostResponse, ExecHostRunResult } from "../infra/exec-host.js";
|
||||
import { resolveExecSafeBinRuntimePolicy } from "../infra/exec-safe-bin-runtime-policy.js";
|
||||
import { sanitizeSystemRunEnvOverrides } from "../infra/host-env-security.js";
|
||||
import {
|
||||
inspectHostExecEnvOverrides,
|
||||
sanitizeSystemRunEnvOverrides,
|
||||
} from "../infra/host-env-security.js";
|
||||
import { normalizeSystemRunApprovalPlan } from "../infra/system-run-approval-binding.js";
|
||||
import { resolveSystemRunCommandRequest } from "../infra/system-run-command.js";
|
||||
import { logWarn } from "../logger.js";
|
||||
@@ -244,6 +247,34 @@ async function parseSystemRunPhase(
|
||||
const sessionKey = opts.params.sessionKey?.trim() || "node";
|
||||
const runId = opts.params.runId?.trim() || crypto.randomUUID();
|
||||
const suppressNotifyOnExit = opts.params.suppressNotifyOnExit === true;
|
||||
const envOverrideDiagnostics = inspectHostExecEnvOverrides({
|
||||
overrides: opts.params.env ?? undefined,
|
||||
blockPathOverrides: true,
|
||||
});
|
||||
if (
|
||||
envOverrideDiagnostics.rejectedOverrideBlockedKeys.length > 0 ||
|
||||
envOverrideDiagnostics.rejectedOverrideInvalidKeys.length > 0
|
||||
) {
|
||||
const details: string[] = [];
|
||||
if (envOverrideDiagnostics.rejectedOverrideBlockedKeys.length > 0) {
|
||||
details.push(
|
||||
`blocked override keys: ${envOverrideDiagnostics.rejectedOverrideBlockedKeys.join(", ")}`,
|
||||
);
|
||||
}
|
||||
if (envOverrideDiagnostics.rejectedOverrideInvalidKeys.length > 0) {
|
||||
details.push(
|
||||
`invalid non-portable override keys: ${envOverrideDiagnostics.rejectedOverrideInvalidKeys.join(", ")}`,
|
||||
);
|
||||
}
|
||||
await opts.sendInvokeResult({
|
||||
ok: false,
|
||||
error: {
|
||||
code: "INVALID_REQUEST",
|
||||
message: `SYSTEM_RUN_DENIED: environment override rejected (${details.join("; ")})`,
|
||||
},
|
||||
});
|
||||
return null;
|
||||
}
|
||||
const envOverrides = sanitizeSystemRunEnvOverrides({
|
||||
overrides: opts.params.env ?? undefined,
|
||||
shellWrapper: shellPayload !== null,
|
||||
|
||||
@@ -51,6 +51,13 @@ describe("node-host sanitizeEnv", () => {
|
||||
expect(env.BASH_ENV).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves inherited non-portable Windows-style env keys", () => {
|
||||
withEnv({ "ProgramFiles(x86)": "C:\\Program Files (x86)" }, () => {
|
||||
const env = sanitizeEnv(undefined);
|
||||
expect(env["ProgramFiles(x86)"]).toBe("C:\\Program Files (x86)");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("node-host output decoding", () => {
|
||||
|
||||
Reference in New Issue
Block a user