fix(exec): detect cmd wrapper carriers (#62439)

* fix(exec): detect cmd wrapper carriers

* fix(exec): block env cmd wrapper carriers

* fix: keep cmd wrapper carriers approval-gated (#62439) (thanks @ngutman)
This commit is contained in:
Nimrod Gutman
2026-04-07 14:27:06 +03:00
committed by GitHub
parent 7d2088132d
commit de6bac331c
5 changed files with 133 additions and 36 deletions

View File

@@ -1600,6 +1600,64 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
}
});
it.each([
{
name: "keeps env cmd.exe transport wrappers approval-gated on Windows",
command: ["env", "cmd.exe", "/d", "/s", "/c"],
},
{
name: "keeps env-assignment cmd.exe transport wrappers approval-gated on Windows",
command: ["env", "FOO=bar", "cmd.exe", "/d", "/s", "/c"],
},
])("$name", async ({ command }) => {
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-env-cmd-wrapper-allow-"));
try {
const scriptPath = path.join(tempDir, "check_mail.cmd");
fs.writeFileSync(scriptPath, "@echo off\r\necho ok\r\n");
const wrappedCommand = [...command, `${scriptPath} --limit 5`];
await withTempApprovalsHome({
approvals: createAllowlistOnMissApprovals({
agents: {
main: {
allowlist: [{ pattern: scriptPath }],
},
},
}),
run: async () => {
const seenArgv: string[][] = [];
const invoke = await runSystemInvoke({
preferMacAppExecHost: false,
command: wrappedCommand,
cwd: tempDir,
security: "allowlist",
ask: "on-miss",
isCmdExeInvocation: (argv) => {
seenArgv.push([...argv]);
const token = argv[0]?.trim();
if (!token) {
return false;
}
const base = path.win32.basename(token).toLowerCase();
return base === "cmd.exe" || base === "cmd";
},
});
expect(seenArgv).toContainEqual(["cmd.exe", "/d", "/s", "/c", `${scriptPath} --limit 5`]);
expect(invoke.runCommand).not.toHaveBeenCalled();
expectApprovalRequiredDenied({
sendNodeEvent: invoke.sendNodeEvent,
sendInvokeResult: invoke.sendInvokeResult,
});
},
});
} finally {
platformSpy.mockRestore();
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
it("reuses exact-command durable trust for shell-wrapper reruns", async () => {
if (process.platform === "win32") {
return;

View File

@@ -20,6 +20,7 @@ import {
detectInterpreterInlineEvalArgv,
} from "../infra/exec-inline-eval.js";
import { resolveExecSafeBinRuntimePolicy } from "../infra/exec-safe-bin-runtime-policy.js";
import { resolveShellWrapperTransportArgv } from "../infra/exec-wrapper-resolution.js";
import {
inspectHostExecEnvOverrides,
sanitizeSystemRunEnvOverrides,
@@ -359,10 +360,11 @@ async function evaluateSystemRunPolicyPhase(
.find((entry) => entry !== null) ?? null)
: null;
const isWindows = process.platform === "win32";
// Detect Windows wrapper transport from the original request argv, not the
// analyzed inner shell payload. Once parsing unwraps `cmd.exe /c ...`, the
// inner segments no longer retain the wrapper marker we need for policy.
const cmdInvocation = opts.isCmdExeInvocation(parsed.argv);
// Detect Windows wrapper transport from the same shell-wrapper view used to
// derive the inner payload. That keeps `cmd.exe /c` approval-gated even when
// dispatch carriers like `env FOO=bar ...` wrap the shell invocation.
const cmdDetectionArgv = resolveShellWrapperTransportArgv(parsed.argv) ?? parsed.argv;
const cmdInvocation = opts.isCmdExeInvocation(cmdDetectionArgv);
const durableApprovalSatisfied = hasDurableExecApproval({
analysisOk,
segmentAllowlistEntries,