mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 16:24:30 +00:00
fix(node-host): enforce system.run rawCommand/argv consistency
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
import type { ExecApprovalManager, ExecApprovalRecord } from "./exec-approval-manager.js";
|
||||
import type { GatewayClient } from "./server-methods/types.js";
|
||||
import {
|
||||
formatExecCommand,
|
||||
validateSystemRunCommandConsistency,
|
||||
} from "../infra/system-run-command.js";
|
||||
|
||||
type SystemRunParamsLike = {
|
||||
command?: unknown;
|
||||
@@ -48,7 +52,7 @@ function getCmdText(params: SystemRunParamsLike): string {
|
||||
if (Array.isArray(params.command)) {
|
||||
const parts = params.command.map((v) => String(v));
|
||||
if (parts.length > 0) {
|
||||
return parts.join(" ");
|
||||
return formatExecCommand(parts);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
@@ -126,6 +130,26 @@ export function sanitizeSystemRunParamsForForwarding(opts: {
|
||||
}
|
||||
|
||||
const p = obj as SystemRunParamsLike;
|
||||
const argv = Array.isArray(p.command) ? p.command.map((v) => String(v)) : [];
|
||||
const raw = normalizeString(p.rawCommand);
|
||||
if (raw) {
|
||||
if (!Array.isArray(p.command) || argv.length === 0) {
|
||||
return {
|
||||
ok: false,
|
||||
message: "rawCommand requires params.command",
|
||||
details: { code: "MISSING_COMMAND" },
|
||||
};
|
||||
}
|
||||
const validation = validateSystemRunCommandConsistency({ argv, rawCommand: raw });
|
||||
if (!validation.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
message: validation.message,
|
||||
details: validation.details ?? { code: "RAW_COMMAND_MISMATCH" },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const approved = p.approved === true;
|
||||
const requestedDecision = normalizeApprovalDecision(p.approvalDecision);
|
||||
const wantsApprovalOverride = approved || requestedDecision !== null;
|
||||
|
||||
@@ -124,6 +124,41 @@ describe("node.invoke approval bypass", () => {
|
||||
return client;
|
||||
};
|
||||
|
||||
test("rejects rawCommand/command mismatch before forwarding to node", async () => {
|
||||
let sawInvoke = false;
|
||||
const node = await connectLinuxNode(() => {
|
||||
sawInvoke = true;
|
||||
});
|
||||
const ws = await connectOperator(["operator.write"]);
|
||||
|
||||
const nodes = await rpcReq<{ nodes?: Array<{ nodeId: string; connected?: boolean }> }>(
|
||||
ws,
|
||||
"node.list",
|
||||
{},
|
||||
);
|
||||
expect(nodes.ok).toBe(true);
|
||||
const nodeId = nodes.payload?.nodes?.find((n) => n.connected)?.nodeId ?? "";
|
||||
expect(nodeId).toBeTruthy();
|
||||
|
||||
const res = await rpcReq(ws, "node.invoke", {
|
||||
nodeId,
|
||||
command: "system.run",
|
||||
params: {
|
||||
command: ["uname", "-a"],
|
||||
rawCommand: "echo hi",
|
||||
},
|
||||
idempotencyKey: crypto.randomUUID(),
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
expect(res.error?.message ?? "").toContain("rawCommand does not match command");
|
||||
|
||||
await sleep(50);
|
||||
expect(sawInvoke).toBe(false);
|
||||
|
||||
ws.close();
|
||||
node.stop();
|
||||
});
|
||||
|
||||
test("rejects injecting approved/approvalDecision without approval id", async () => {
|
||||
let sawInvoke = false;
|
||||
const node = await connectLinuxNode(() => {
|
||||
|
||||
Reference in New Issue
Block a user