# 漏洞总结 ## 漏洞概述 OpenClaw 平台存在权限控制缺陷,允许非管理员用户在特定条件下执行需要管理员权限的配置写入命令(如 `arm`、`disarm` 电话控制命令)。攻击者可通过构造特定请求绕过权限检查,实现对电话控制系统的未授权操作。 ## 影响范围 - 受影响组件:`extensions/phone-control/index.ts` 和 `extensions/talk-voice/index.ts` - 影响功能:电话控制插件(phone-control plugin)和语音控制插件(talk-voice plugin) - 影响场景: - 外部渠道调用者(如 Telegram、Webchat)无需 `operator.admin` 权限即可执行配置写入 - 内部网关调用者同样存在权限绕过风险 - 语音设置功能(set voice)也存在类似权限绕过问题 ## 修复方案 1. **权限检查强化**:在 `phone-control/index.ts` 中添加对 `operator.admin` 权限的严格检查 2. **渠道隔离**:区分内部网关调用和外部渠道调用,对外部渠道实施更严格的权限验证 3. **错误消息改进**:当权限不足时返回明确的错误信息(如 "requires operator.admin") 4. **测试覆盖**:增加针对权限控制的测试用例,确保各种场景下的权限验证正常工作 ## POC代码 ```typescript // 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); }); }); // 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"); }); ```