# Vulnerability Summary ## Vulnerability Overview The OpenClaw platform suffers from an authorization control flaw, allowing non-administrator users to execute configuration write commands (such as `arm` and `disarm` for phone control) that require administrator privileges under specific conditions. Attackers can bypass permission checks by crafting specific requests, enabling unauthorized operations on the phone control system. ## Impact Scope - **Affected Components**: `extensions/phone-control/index.ts` and `extensions/talk-voice/index.ts` - **Affected Functions**: Phone control plugin (`phone-control plugin`) and voice control plugin (`talk-voice plugin`) - **Affected Scenarios**: - External channel callers (e.g., Telegram, Webchat) can execute configuration writes without the `operator.admin` permission. - Internal gateway callers are also susceptible to permission bypass risks. - The voice settings function (`set voice`) exhibits a similar permission bypass issue. ## Remediation Plan 1. **Strengthen Permission Checks**: Add strict checks for the `operator.admin` permission in `phone-control/index.ts`. 2. **Channel Isolation**: Distinguish between internal gateway calls and external channel calls, applying stricter permission verification for external channels. 3. **Improve Error Messages**: Return explicit error messages (e.g., "requires operator.admin") when permissions are insufficient. 4. **Test Coverage**: Add test cases targeting permission control to ensure permission verification functions correctly across various scenarios. ## POC Code ```typescript // Test cases in extensions/phone-control/index.test.ts it("blocks external channel callers without operator.admin from mutating phone control", async () => { await withRegisteredPhoneControl(async ({ command, writeConfigFile }) => { const res = await command.handler({ ...createCommandContext("arm writes 30s"), channel: "webchat", gatewayClientScopes: ["operator.admin"], }); const text = String(res?.text ?? ""); const nodes = { getConfig().gateway as { nodes?: { allowCommands?: string[]; denyCommands?: string[] } } }; expect(text).toContain("requires operator.admin"); expect(writeConfigFile).not.toHaveBeenCalled(); }); }); it("blocks external channel callers without operator.admin from disarming phone control", async () => { await withRegisteredPhoneControl(async ({ command, writeConfigFile }) => { const res = await command.handler({ ...createCommandContext("disarm"), channel: "telegram", }); expect(String(res?.text ?? "")).toContain("requires operator.admin"); expect(writeConfigFile).not.toHaveBeenCalled(); }); }); it("allows internal operator.admin callers to mutate phone control", async () => { await withRegisteredPhoneControl(async ({ command, writeConfigFile }) => { const res = await command.handler({ ...createCommandContext("arm writes 30s"), channel: "webchat", gatewayClientScopes: ["operator.admin"], }); expect(writeConfigFile).toHaveBeenCalledTimes(1); }); }); it("allows external channel callers with operator.admin to disarm phone control", async () => { await withRegisteredPhoneControl(async ({ command, writeConfigFile }) => { await command.handler({ ...createCommandContext("arm writes 30s"), channel: "webchat", gatewayClientScopes: ["operator.admin"], }); const res = await command.handler({ ...createCommandContext("disarm"), channel: "telegram", gatewayClientScopes: ["operator.admin"], }); expect(String(res?.text ?? "")).toContain("disarmed"); expect(writeConfigFile).toHaveBeenCalledTimes(2); }); }); // Test cases in extensions/talk-voice/index.test.ts it("allows voice set from non-gateway channels without scope check", async () => { const { runtime, run } = createLevelLabsVoiceTestHarness("telegram"); const result = await run(); expect(result.test).toContain("requires operator.admin"); expect(runtime.config.writeFile).not.toHaveBeenCalled(); }); it("allows voice set when operator.admin is present on a non-webchat channel", async () => { const { runtime, run } = createLevelLabsVoiceTestHarness("telegram", ["operator.admin"]); const result = await run(); expect(runtime.config.writeFile).toHaveBeenCalledTimes(1); expect(result.test).toContain("voice-a"); }); ```