mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 19:11:23 +00:00
fix(node-host): enforce system.run rawCommand/argv consistency
This commit is contained in:
54
src/infra/system-run-command.test.ts
Normal file
54
src/infra/system-run-command.test.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import {
|
||||
extractShellCommandFromArgv,
|
||||
formatExecCommand,
|
||||
validateSystemRunCommandConsistency,
|
||||
} from "./system-run-command.js";
|
||||
|
||||
describe("system run command helpers", () => {
|
||||
test("formatExecCommand quotes args with spaces", () => {
|
||||
expect(formatExecCommand(["echo", "hi there"])).toBe('echo "hi there"');
|
||||
});
|
||||
|
||||
test("extractShellCommandFromArgv extracts sh -lc command", () => {
|
||||
expect(extractShellCommandFromArgv(["/bin/sh", "-lc", "echo hi"])).toBe("echo hi");
|
||||
});
|
||||
|
||||
test("extractShellCommandFromArgv extracts cmd.exe /c command", () => {
|
||||
expect(extractShellCommandFromArgv(["cmd.exe", "/d", "/s", "/c", "echo hi"])).toBe("echo hi");
|
||||
});
|
||||
|
||||
test("validateSystemRunCommandConsistency accepts rawCommand matching direct argv", () => {
|
||||
const res = validateSystemRunCommandConsistency({
|
||||
argv: ["echo", "hi"],
|
||||
rawCommand: "echo hi",
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
if (!res.ok) {
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
expect(res.shellCommand).toBe(null);
|
||||
expect(res.cmdText).toBe("echo hi");
|
||||
});
|
||||
|
||||
test("validateSystemRunCommandConsistency rejects mismatched rawCommand vs direct argv", () => {
|
||||
const res = validateSystemRunCommandConsistency({
|
||||
argv: ["uname", "-a"],
|
||||
rawCommand: "echo hi",
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
if (res.ok) {
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
expect(res.message).toContain("rawCommand does not match command");
|
||||
expect(res.details?.code).toBe("RAW_COMMAND_MISMATCH");
|
||||
});
|
||||
|
||||
test("validateSystemRunCommandConsistency accepts rawCommand matching sh wrapper argv", () => {
|
||||
const res = validateSystemRunCommandConsistency({
|
||||
argv: ["/bin/sh", "-lc", "echo hi"],
|
||||
rawCommand: "echo hi",
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
});
|
||||
});
|
||||
106
src/infra/system-run-command.ts
Normal file
106
src/infra/system-run-command.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import path from "node:path";
|
||||
|
||||
export type SystemRunCommandValidation =
|
||||
| {
|
||||
ok: true;
|
||||
shellCommand: string | null;
|
||||
cmdText: string;
|
||||
}
|
||||
| {
|
||||
ok: false;
|
||||
message: string;
|
||||
details?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
function basenameLower(token: string): string {
|
||||
const win = path.win32.basename(token);
|
||||
const posix = path.posix.basename(token);
|
||||
const base = win.length < posix.length ? win : posix;
|
||||
return base.trim().toLowerCase();
|
||||
}
|
||||
|
||||
export function formatExecCommand(argv: string[]): string {
|
||||
return argv
|
||||
.map((arg) => {
|
||||
const trimmed = arg.trim();
|
||||
if (!trimmed) {
|
||||
return '""';
|
||||
}
|
||||
const needsQuotes = /\s|"/.test(trimmed);
|
||||
if (!needsQuotes) {
|
||||
return trimmed;
|
||||
}
|
||||
return `"${trimmed.replace(/"/g, '\\"')}"`;
|
||||
})
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
export function extractShellCommandFromArgv(argv: string[]): string | null {
|
||||
const token0 = argv[0]?.trim();
|
||||
if (!token0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const base0 = basenameLower(token0);
|
||||
|
||||
// POSIX-style shells: sh -lc "<cmd>"
|
||||
if (
|
||||
base0 === "sh" ||
|
||||
base0 === "bash" ||
|
||||
base0 === "zsh" ||
|
||||
base0 === "dash" ||
|
||||
base0 === "ksh"
|
||||
) {
|
||||
const flag = argv[1]?.trim();
|
||||
if (flag !== "-lc" && flag !== "-c") {
|
||||
return null;
|
||||
}
|
||||
const cmd = argv[2];
|
||||
return typeof cmd === "string" ? cmd : null;
|
||||
}
|
||||
|
||||
// Windows cmd.exe: cmd.exe /d /s /c "<cmd>"
|
||||
if (base0 === "cmd.exe" || base0 === "cmd") {
|
||||
const idx = argv.findIndex((item) => String(item).trim().toLowerCase() === "/c");
|
||||
if (idx === -1) {
|
||||
return null;
|
||||
}
|
||||
const cmd = argv[idx + 1];
|
||||
return typeof cmd === "string" ? cmd : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function validateSystemRunCommandConsistency(params: {
|
||||
argv: string[];
|
||||
rawCommand?: string | null;
|
||||
}): SystemRunCommandValidation {
|
||||
const raw =
|
||||
typeof params.rawCommand === "string" && params.rawCommand.trim().length > 0
|
||||
? params.rawCommand.trim()
|
||||
: null;
|
||||
const shellCommand = extractShellCommandFromArgv(params.argv);
|
||||
const inferred = shellCommand ? shellCommand.trim() : formatExecCommand(params.argv);
|
||||
|
||||
if (raw && raw !== inferred) {
|
||||
return {
|
||||
ok: false,
|
||||
message: "INVALID_REQUEST: rawCommand does not match command",
|
||||
details: {
|
||||
code: "RAW_COMMAND_MISMATCH",
|
||||
rawCommand: raw,
|
||||
inferred,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
// Only treat this as a shell command when argv is a recognized shell wrapper.
|
||||
// For direct argv execution, rawCommand is purely display/approval text and
|
||||
// must match the formatted argv.
|
||||
shellCommand: shellCommand ? (raw ?? shellCommand) : null,
|
||||
cmdText: raw ?? shellCommand ?? inferred,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user