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:
Josh Avant
2026-03-20 15:44:15 -05:00
committed by GitHub
parent c7134e629c
commit 7abfff756d
14 changed files with 510 additions and 47 deletions

View File

@@ -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;

View File

@@ -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,

View File

@@ -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", () => {