mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-08 06:54:24 +00:00
fix(security): harden sms.send dangerous-node defaults
This commit is contained in:
@@ -44,6 +44,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Gateway/Node dangerous-command parity: include `sms.send` in default onboarding node `denyCommands`, share onboarding deny defaults with the gateway dangerous-command source of truth, and include `sms.send` in phone-control `/phone arm writes` handling so SMS follows the same break-glass flow as other dangerous node commands. Thanks @zpbrent.
|
||||
- Zalo/Pairing auth tests: add webhook regression coverage asserting DM pairing-store reads/writes remain account-scoped, preventing cross-account authorization bleed in multi-account setups. (#26121) Thanks @bmendonca3.
|
||||
- Logging: use local time for logged timestamps instead of UTC, aligning log output with documented local timezone behavior and avoiding confusion during local diagnostics. (#28434) Thanks @liuy.
|
||||
- Zalouser/Pairing auth tests: add account-scoped DM pairing-store regression coverage (`monitor.account-scope.test.ts`) to prevent cross-account allowlist bleed in multi-account setups. (#26672) Thanks @bmendonca3.
|
||||
|
||||
110
extensions/phone-control/index.test.ts
Normal file
110
extensions/phone-control/index.test.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type {
|
||||
OpenClawPluginApi,
|
||||
OpenClawPluginCommandDefinition,
|
||||
PluginCommandContext,
|
||||
} from "../../src/plugins/types.js";
|
||||
import registerPhoneControl from "./index.js";
|
||||
|
||||
function createApi(params: {
|
||||
stateDir: string;
|
||||
getConfig: () => Record<string, unknown>;
|
||||
writeConfig: (next: Record<string, unknown>) => Promise<void>;
|
||||
registerCommand: (command: OpenClawPluginCommandDefinition) => void;
|
||||
}): OpenClawPluginApi {
|
||||
return {
|
||||
id: "phone-control",
|
||||
name: "phone-control",
|
||||
source: "test",
|
||||
config: {},
|
||||
pluginConfig: {},
|
||||
runtime: {
|
||||
state: {
|
||||
resolveStateDir: () => params.stateDir,
|
||||
},
|
||||
config: {
|
||||
loadConfig: () => params.getConfig(),
|
||||
writeConfigFile: (next: Record<string, unknown>) => params.writeConfig(next),
|
||||
},
|
||||
} as OpenClawPluginApi["runtime"],
|
||||
logger: { info() {}, warn() {}, error() {} },
|
||||
registerTool() {},
|
||||
registerHook() {},
|
||||
registerHttpHandler() {},
|
||||
registerHttpRoute() {},
|
||||
registerChannel() {},
|
||||
registerGatewayMethod() {},
|
||||
registerCli() {},
|
||||
registerService() {},
|
||||
registerProvider() {},
|
||||
registerCommand: params.registerCommand,
|
||||
resolvePath(input: string) {
|
||||
return input;
|
||||
},
|
||||
on() {},
|
||||
};
|
||||
}
|
||||
|
||||
function createCommandContext(args: string): PluginCommandContext {
|
||||
return {
|
||||
channel: "test",
|
||||
isAuthorizedSender: true,
|
||||
commandBody: `/phone ${args}`,
|
||||
args,
|
||||
config: {},
|
||||
};
|
||||
}
|
||||
|
||||
describe("phone-control plugin", () => {
|
||||
it("arms sms.send as part of the writes group", async () => {
|
||||
const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-phone-control-test-"));
|
||||
try {
|
||||
let config: Record<string, unknown> = {
|
||||
gateway: {
|
||||
nodes: {
|
||||
allowCommands: [],
|
||||
denyCommands: ["calendar.add", "contacts.add", "reminders.add", "sms.send"],
|
||||
},
|
||||
},
|
||||
};
|
||||
const writeConfigFile = vi.fn(async (next: Record<string, unknown>) => {
|
||||
config = next;
|
||||
});
|
||||
|
||||
let command: OpenClawPluginCommandDefinition | undefined;
|
||||
registerPhoneControl(
|
||||
createApi({
|
||||
stateDir,
|
||||
getConfig: () => config,
|
||||
writeConfig: writeConfigFile,
|
||||
registerCommand: (nextCommand) => {
|
||||
command = nextCommand;
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(command?.name).toBe("phone");
|
||||
|
||||
const res = await command?.handler(createCommandContext("arm writes 30s"));
|
||||
const text = String(res?.text ?? "");
|
||||
const nodes = (
|
||||
config.gateway as { nodes?: { allowCommands?: string[]; denyCommands?: string[] } }
|
||||
).nodes;
|
||||
|
||||
expect(writeConfigFile).toHaveBeenCalledTimes(1);
|
||||
expect(nodes?.allowCommands).toEqual([
|
||||
"calendar.add",
|
||||
"contacts.add",
|
||||
"reminders.add",
|
||||
"sms.send",
|
||||
]);
|
||||
expect(nodes?.denyCommands).toEqual([]);
|
||||
expect(text).toContain("sms.send");
|
||||
} finally {
|
||||
await fs.rm(stateDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -29,7 +29,7 @@ const STATE_REL_PATH = ["plugins", "phone-control", "armed.json"] as const;
|
||||
const GROUP_COMMANDS: Record<Exclude<ArmGroup, "all">, string[]> = {
|
||||
camera: ["camera.snap", "camera.clip"],
|
||||
screen: ["screen.record"],
|
||||
writes: ["calendar.add", "contacts.add", "reminders.add"],
|
||||
writes: ["calendar.add", "contacts.add", "reminders.add", "sms.send"],
|
||||
};
|
||||
|
||||
function uniqSorted(values: string[]): string[] {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createWizardPrompter as buildWizardPrompter } from "../../test/helpers/wizard-prompter.js";
|
||||
import { DEFAULT_DANGEROUS_NODE_COMMANDS } from "../gateway/node-command-policy.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import type { WizardPrompter, WizardSelectParams } from "./prompts.js";
|
||||
|
||||
@@ -89,14 +90,7 @@ describe("configureGatewayForOnboarding", () => {
|
||||
const result = await runGatewayConfig();
|
||||
|
||||
expect(result.settings.gatewayToken).toBe("generated-token");
|
||||
expect(result.nextConfig.gateway?.nodes?.denyCommands).toEqual([
|
||||
"camera.snap",
|
||||
"camera.clip",
|
||||
"screen.record",
|
||||
"calendar.add",
|
||||
"contacts.add",
|
||||
"reminders.add",
|
||||
]);
|
||||
expect(result.nextConfig.gateway?.nodes?.denyCommands).toEqual(DEFAULT_DANGEROUS_NODE_COMMANDS);
|
||||
});
|
||||
|
||||
it("prefers OPENCLAW_GATEWAY_TOKEN during quickstart token setup", async () => {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
TAILSCALE_EXPOSURE_OPTIONS,
|
||||
TAILSCALE_MISSING_BIN_NOTE_LINES,
|
||||
} from "../gateway/gateway-config-prompts.shared.js";
|
||||
import { DEFAULT_DANGEROUS_NODE_COMMANDS } from "../gateway/node-command-policy.js";
|
||||
import { findTailscaleBinary } from "../infra/tailscale.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { validateIPv4AddressInput } from "../shared/net/ipv4.js";
|
||||
@@ -22,20 +23,6 @@ import type {
|
||||
} from "./onboarding.types.js";
|
||||
import type { WizardPrompter } from "./prompts.js";
|
||||
|
||||
// These commands are "high risk" (privacy writes/recording) and should be
|
||||
// explicitly armed by the user when they want to use them.
|
||||
//
|
||||
// This only affects what the gateway will accept via node.invoke; the iOS app
|
||||
// still prompts for OS permissions (camera/photos/contacts/etc) on first use.
|
||||
const DEFAULT_DANGEROUS_NODE_DENY_COMMANDS = [
|
||||
"camera.snap",
|
||||
"camera.clip",
|
||||
"screen.record",
|
||||
"calendar.add",
|
||||
"contacts.add",
|
||||
"reminders.add",
|
||||
];
|
||||
|
||||
type ConfigureGatewayOptions = {
|
||||
flow: WizardFlow;
|
||||
baseConfig: OpenClawConfig;
|
||||
@@ -250,7 +237,7 @@ export async function configureGatewayForOnboarding(
|
||||
...nextConfig.gateway,
|
||||
nodes: {
|
||||
...nextConfig.gateway?.nodes,
|
||||
denyCommands: [...DEFAULT_DANGEROUS_NODE_DENY_COMMANDS],
|
||||
denyCommands: [...DEFAULT_DANGEROUS_NODE_COMMANDS],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user