mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 23:18:28 +00:00
fix(gateway): block node.invoke exec approvals
This commit is contained in:
@@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
|
|
||||||
- BlueBubbles: include sender identity in group chat envelopes and pass clean message text to the agent prompt, aligning with iMessage/Signal formatting. (#16210) Thanks @zerone0x.
|
- BlueBubbles: include sender identity in group chat envelopes and pass clean message text to the agent prompt, aligning with iMessage/Signal formatting. (#16210) Thanks @zerone0x.
|
||||||
- Security/Node Host: enforce `system.run` rawCommand/argv consistency to prevent allowlist/approval bypass. Thanks @christos-eth.
|
- Security/Node Host: enforce `system.run` rawCommand/argv consistency to prevent allowlist/approval bypass. Thanks @christos-eth.
|
||||||
|
- Security/Gateway: block `system.execApprovals.*` via `node.invoke` (use `exec.approvals.node.*` instead). Thanks @christos-eth.
|
||||||
- CLI: fix lazy core command registration so top-level maintenance commands (`doctor`, `dashboard`, `reset`, `uninstall`) resolve correctly instead of exposing a non-functional `maintenance` placeholder command.
|
- CLI: fix lazy core command registration so top-level maintenance commands (`doctor`, `dashboard`, `reset`, `uninstall`) resolve correctly instead of exposing a non-functional `maintenance` placeholder command.
|
||||||
- Security/Agents: scope CLI process cleanup to owned child PIDs to avoid killing unrelated processes on shared hosts. Thanks @aether-ai-agent.
|
- Security/Agents: scope CLI process cleanup to owned child PIDs to avoid killing unrelated processes on shared hosts. Thanks @aether-ai-agent.
|
||||||
- Security/Agents (macOS): prevent shell injection when writing Claude CLI keychain credentials. (#15924) Thanks @aether-ai-agent.
|
- Security/Agents (macOS): prevent shell injection when writing Claude CLI keychain credentials. (#15924) Thanks @aether-ai-agent.
|
||||||
|
|||||||
@@ -39,14 +39,7 @@ const SMS_DANGEROUS_COMMANDS = ["sms.send"];
|
|||||||
// iOS nodes don't implement system.run/which, but they do support notifications.
|
// iOS nodes don't implement system.run/which, but they do support notifications.
|
||||||
const IOS_SYSTEM_COMMANDS = ["system.notify"];
|
const IOS_SYSTEM_COMMANDS = ["system.notify"];
|
||||||
|
|
||||||
const SYSTEM_COMMANDS = [
|
const SYSTEM_COMMANDS = ["system.run", "system.which", "system.notify", "browser.proxy"];
|
||||||
"system.run",
|
|
||||||
"system.which",
|
|
||||||
"system.notify",
|
|
||||||
"system.execApprovals.get",
|
|
||||||
"system.execApprovals.set",
|
|
||||||
"browser.proxy",
|
|
||||||
];
|
|
||||||
|
|
||||||
// "High risk" node commands. These can be enabled by explicitly adding them to
|
// "High risk" node commands. These can be enabled by explicitly adding them to
|
||||||
// `gateway.nodes.allowCommands` (and ensuring they're not blocked by denyCommands).
|
// `gateway.nodes.allowCommands` (and ensuring they're not blocked by denyCommands).
|
||||||
|
|||||||
@@ -388,6 +388,18 @@ export const nodeHandlers: GatewayRequestHandlers = {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (command === "system.execApprovals.get" || command === "system.execApprovals.set") {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(
|
||||||
|
ErrorCodes.INVALID_REQUEST,
|
||||||
|
"node.invoke does not allow system.execApprovals.*; use exec.approvals.node.*",
|
||||||
|
{ details: { command } },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await respondUnavailableOnThrow(respond, async () => {
|
await respondUnavailableOnThrow(respond, async () => {
|
||||||
const nodeSession = context.nodeRegistry.get(nodeId);
|
const nodeSession = context.nodeRegistry.get(nodeId);
|
||||||
|
|||||||
@@ -197,6 +197,38 @@ describe("node.invoke approval bypass", () => {
|
|||||||
node.stop();
|
node.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("rejects invoking system.execApprovals.set via node.invoke", 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.execApprovals.set",
|
||||||
|
params: { file: { version: 1, agents: {} }, baseHash: "nope" },
|
||||||
|
idempotencyKey: crypto.randomUUID(),
|
||||||
|
});
|
||||||
|
expect(res.ok).toBe(false);
|
||||||
|
expect(res.error?.message ?? "").toContain("exec.approvals.node");
|
||||||
|
|
||||||
|
await sleep(50);
|
||||||
|
expect(sawInvoke).toBe(false);
|
||||||
|
|
||||||
|
ws.close();
|
||||||
|
node.stop();
|
||||||
|
});
|
||||||
|
|
||||||
test("binds system.run approval flags to exec.approval decision (ignores caller escalation)", async () => {
|
test("binds system.run approval flags to exec.approval decision (ignores caller escalation)", async () => {
|
||||||
let lastInvokeParams: Record<string, unknown> | null = null;
|
let lastInvokeParams: Record<string, unknown> | null = null;
|
||||||
const node = await connectLinuxNode((payload) => {
|
const node = await connectLinuxNode((payload) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user