mirror of https://github.com/openclaw/openclaw.git
qqbot: require explicit allowlist for /bot-logs to prevent info disclosure (#58895)
* qqbot: harden /bot-logs authorization fallback * fix(qqbot): harden bot logs allowlist guard * fix(qqbot): normalize bot logs allowlist entries
This commit is contained in:
parent
07c60ae461
commit
72af92ba4e
|
|
@ -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.<id>` 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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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> = {}): 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。",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -108,6 +108,20 @@ export interface QQBotFrameworkCommand {
|
|||
handler: (ctx: SlashCommandContext) => SlashCommandResult | Promise<SlashCommandResult>;
|
||||
}
|
||||
|
||||
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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue