diff --git a/CHANGELOG.md b/CHANGELOG.md index eee899737e2..1564ab3b3ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ Docs: https://docs.openclaw.ai - Sandbox/browser: compare browser runtime inspection against `agents.defaults.sandbox.browser.image` so `openclaw sandbox list --browser` stops reporting healthy browser containers as image mismatches. (#58759) Thanks @sandpile. - Exec/approvals: resume the original agent session after an approved async exec finishes, so external completion followups continue the task instead of waiting for a new user turn. (#58860) Thanks @Nanako0129. - Channels/plugins: keep bundled channel plugins loadable from legacy `channels.` config even under restrictive plugin allowlists, and make `openclaw doctor` warn only on real plugin blockers instead of misleading setup guidance. (#58873) Thanks @obviyus +- Channels/QQ Bot: keep `/bot-logs` export gated behind a truly explicit QQBot allowlist, rejecting wildcard and mixed wildcard entries while preserving the real framework command path. Thanks @vincentkoc. - Auto-reply/commands: strip inbound metadata before slash command detection so wrapped `/model`, `/new`, and `/status` commands are recognized. (#58725) Thanks @Mlightsnow. - Gateway/nodes: stop pinning live node commands to the approved node-pair record. Node pairing remains a trust/token flow, while per-node `system.run` policy stays in that node's exec approvals config. Fixes #58824. - WebChat/exec approvals: use native approval UI guidance in agent system prompts instead of telling agents to paste manual `/approve` commands in webchat sessions. Thanks @vincentkoc. diff --git a/extensions/qqbot/index.ts b/extensions/qqbot/index.ts index a00e9ba052d..62ded34412a 100644 --- a/extensions/qqbot/index.ts +++ b/extensions/qqbot/index.ts @@ -56,6 +56,7 @@ export default defineChannelPluginEntry({ : rawMsgType === "dm" ? "dm" : "c2c"; + const account = resolveQQBotAccount(ctx.config, ctx.accountId ?? undefined); // Build a minimal SlashCommandContext from the framework PluginCommandContext. // commandAuthorized is always true here because the framework has already @@ -68,10 +69,11 @@ export default defineChannelPluginEntry({ receivedAt: Date.now(), rawContent: `/${cmd.name}${ctx.args ? ` ${ctx.args}` : ""}`, args: ctx.args ?? "", - accountId: ctx.accountId ?? "default", + accountId: account.accountId, // appId is not available from PluginCommandContext directly; handlers // that need it should call resolveQQBotAccount(ctx.config, ctx.accountId). - appId: "", + appId: account.appId, + accountConfig: account.config, commandAuthorized: true, queueSnapshot: { totalPending: 0, @@ -91,7 +93,6 @@ export default defineChannelPluginEntry({ // File result: send the file attachment via QQ API, return text summary. if (result && "filePath" in result) { try { - const account = resolveQQBotAccount(ctx.config, ctx.accountId ?? undefined); const mediaCtx: MediaTargetContext = { targetType, targetId, diff --git a/extensions/qqbot/src/slash-commands.test.ts b/extensions/qqbot/src/slash-commands.test.ts index 1fbac9cf61a..1aa26d01e18 100644 --- a/extensions/qqbot/src/slash-commands.test.ts +++ b/extensions/qqbot/src/slash-commands.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from "vitest"; -import { matchSlashCommand, type SlashCommandContext } from "./slash-commands.js"; +import { + getFrameworkCommands, + matchSlashCommand, + type SlashCommandContext, +} from "./slash-commands.js"; /** Build a minimal SlashCommandContext for testing. */ function buildCtx(overrides: Partial = {}): SlashCommandContext { @@ -110,3 +114,58 @@ describe("slash command authorization", () => { // ---- usage query (?) for remaining pre-dispatch commands ---- }); + +describe("/bot-logs framework command hardening", () => { + function getBotLogsHandler() { + const command = getFrameworkCommands().find((item) => item.name === "bot-logs"); + expect(command).toBeDefined(); + return command!.handler; + } + + it("rejects /bot-logs when allowFrom is wildcard", async () => { + const handler = getBotLogsHandler(); + const result = await handler(buildCtx({ accountConfig: { allowFrom: ["*"] } })); + expect(result).toBeTypeOf("string"); + expect(result as string).toContain("权限不足"); + }); + + it("rejects /bot-logs when allowFrom mixes wildcard and explicit entries", async () => { + const handler = getBotLogsHandler(); + const result = await handler(buildCtx({ accountConfig: { allowFrom: ["*", "qqbot:user-1"] } })); + expect(result).toBeTypeOf("string"); + expect(result as string).toContain("权限不足"); + }); + + it("rejects /bot-logs when allowFrom uses qqbot:* wildcard form", async () => { + const handler = getBotLogsHandler(); + const result = await handler(buildCtx({ accountConfig: { allowFrom: ["qqbot:*"] } })); + expect(result).toBeTypeOf("string"); + expect(result as string).toContain("权限不足"); + }); + + it("rejects /bot-logs when allowFrom uses qqbot: * wildcard form", async () => { + const handler = getBotLogsHandler(); + const result = await handler(buildCtx({ accountConfig: { allowFrom: ["qqbot: *"] } })); + expect(result).toBeTypeOf("string"); + expect(result as string).toContain("权限不足"); + }); + + it("allows /bot-logs when allowFrom contains numeric sender ids", async () => { + const handler = getBotLogsHandler(); + const accountConfig = { allowFrom: [12345] } as unknown as SlashCommandContext["accountConfig"]; + const result = await handler(buildCtx({ accountConfig })); + expect(result).not.toBeNull(); + expect(result).not.toBe( + "⛔ 权限不足:请先在 channels.qqbot.allowFrom(或对应账号 allowFrom)中配置明确的发送者列表后再使用 /bot-logs。", + ); + }); + + it("allows /bot-logs execution when allowFrom is explicit", async () => { + const handler = getBotLogsHandler(); + const result = await handler(buildCtx({ accountConfig: { allowFrom: ["qqbot:user-1"] } })); + expect(result).not.toBeNull(); + expect(result).not.toBe( + "⛔ 权限不足:请先在 channels.qqbot.allowFrom(或对应账号 allowFrom)中配置明确的发送者列表后再使用 /bot-logs。", + ); + }); +}); diff --git a/extensions/qqbot/src/slash-commands.ts b/extensions/qqbot/src/slash-commands.ts index 8b4ffaf2642..1b95adad522 100644 --- a/extensions/qqbot/src/slash-commands.ts +++ b/extensions/qqbot/src/slash-commands.ts @@ -108,6 +108,20 @@ export interface QQBotFrameworkCommand { handler: (ctx: SlashCommandContext) => SlashCommandResult | Promise; } +function hasExplicitCommandAllowlist(accountConfig?: QQBotAccountConfig): boolean { + const allowFrom = accountConfig?.allowFrom; + if (!Array.isArray(allowFrom) || allowFrom.length === 0) { + return false; + } + return allowFrom.every((entry) => { + const normalized = String(entry) + .trim() + .replace(/^qqbot:\s*/i, "") + .trim(); + return normalized.length > 0 && normalized !== "*"; + }); +} + // ============ Command registry ============ // Pre-dispatch commands (requireAuth: false) — handled immediately before queuing. @@ -529,7 +543,14 @@ registerCommand({ `导出最近的 OpenClaw 日志文件(最多 4 个文件)。`, `每个文件只保留最后 1000 行,并作为附件返回。`, ].join("\n"), - handler: () => buildBotLogsResult(), + handler: (ctx) => { + // Defense in depth: require an explicit QQ allowlist entry for log export. + // This keeps `/bot-logs` closed when setup leaves allowFrom in permissive mode. + if (!hasExplicitCommandAllowlist(ctx.accountConfig)) { + return `⛔ 权限不足:请先在 channels.qqbot.allowFrom(或对应账号 allowFrom)中配置明确的发送者列表后再使用 /bot-logs。`; + } + return buildBotLogsResult(); + }, }); // Slash command entry point.