From a32fe807f405b0d07c70b08ed940a1a91ad8988b Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 23 Apr 2026 11:33:24 -0700 Subject: [PATCH] fix(discord): block channel policy auth bypass --- CHANGELOG.md | 1 + .../native-command.commands-allowfrom.test.ts | 61 +++++++++++++++++++ .../native-command.plugin-dispatch.test.ts | 8 +-- .../discord/src/monitor/native-command.ts | 7 ++- 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 057835521cc..28bae66f880 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Discord/security: keep native slash-command channel policy from bypassing configured owner or member restrictions, while preserving channel-policy fallback when no stricter access rule exists. (#70711) Thanks @vincentkoc. - Android/security: stop `ASK_OPENCLAW` intents from auto-sending injected prompts, so external app actions only prefill the draft instead of dispatching it immediately. (#70714) Thanks @vincentkoc. - Control UI/chat: queue Stop-button aborts across Gateway reconnects so a disconnected active run is canceled on reconnect instead of only clearing local UI state. (#70673) Thanks @chinar-amrutkar. - Secrets/Windows: strip UTF-8 BOMs from file-backed secrets and keep unavailable ACL checks fail-closed unless trusted file or exec providers explicitly opt into `allowInsecurePath`. (#70662) Thanks @zhanggpcsu. diff --git a/extensions/discord/src/monitor/native-command.commands-allowfrom.test.ts b/extensions/discord/src/monitor/native-command.commands-allowfrom.test.ts index c44d407a1c7..d62ba6995cb 100644 --- a/extensions/discord/src/monitor/native-command.commands-allowfrom.test.ts +++ b/extensions/discord/src/monitor/native-command.commands-allowfrom.test.ts @@ -230,6 +230,67 @@ describe("Discord native slash commands with commands.allowFrom", () => { expectNotUnauthorizedReply(interaction); }); + it("authorizes guild slash commands when channel access restrictions include the member", async () => { + const { dispatchSpy, interaction } = await runGuildSlashCommand({ + mutateConfig: (cfg) => { + cfg.commands = { + ...cfg.commands, + allowFrom: undefined, + }; + cfg.channels = { + ...cfg.channels, + discord: { + ...cfg.channels?.discord, + guilds: { + "345678901234567890": { + channels: { + "234567890123456789": { + enabled: true, + requireMention: false, + users: ["user:123456789012345678"], + }, + }, + }, + }, + }, + }; + }, + }); + expect(dispatchSpy).toHaveBeenCalledTimes(1); + expectNotUnauthorizedReply(interaction); + }); + + it("rejects guild slash commands when channel access restrictions exclude the member even if channel policy would allow them", async () => { + const { dispatchSpy, interaction } = await runGuildSlashCommand({ + userId: "999999999999999999", + mutateConfig: (cfg) => { + cfg.commands = { + ...cfg.commands, + allowFrom: undefined, + }; + cfg.channels = { + ...cfg.channels, + discord: { + ...cfg.channels?.discord, + guilds: { + "345678901234567890": { + channels: { + "234567890123456789": { + enabled: true, + requireMention: false, + users: ["user:123456789012345678"], + }, + }, + }, + }, + }, + }; + }, + }); + expect(dispatchSpy).not.toHaveBeenCalled(); + expectUnauthorizedReply(interaction); + }); + it("rejects guild slash commands outside the Discord allowlist when commands.useAccessGroups is false and commands.allowFrom is not configured", async () => { const { dispatchSpy, interaction } = await runGuildSlashCommand({ mutateConfig: (cfg) => { diff --git a/extensions/discord/src/monitor/native-command.plugin-dispatch.test.ts b/extensions/discord/src/monitor/native-command.plugin-dispatch.test.ts index cb235732fc2..78927b24b60 100644 --- a/extensions/discord/src/monitor/native-command.plugin-dispatch.test.ts +++ b/extensions/discord/src/monitor/native-command.plugin-dispatch.test.ts @@ -543,12 +543,12 @@ describe("Discord native plugin command dispatch", () => { "thread-123": { enabled: true, requireMention: false, - users: ["owner"], + users: ["user:owner"], }, "parent-456": { enabled: true, requireMention: false, - users: ["owner"], + users: ["user:owner"], }, }, }, @@ -613,12 +613,12 @@ describe("Discord native plugin command dispatch", () => { "partial-thread-123": { enabled: true, requireMention: false, - users: ["owner"], + users: ["user:owner"], }, "partial-parent-456": { enabled: true, requireMention: false, - users: ["owner"], + users: ["user:owner"], }, }, }, diff --git a/extensions/discord/src/monitor/native-command.ts b/extensions/discord/src/monitor/native-command.ts index de26ec7cbca..ae2e958183d 100644 --- a/extensions/discord/src/monitor/native-command.ts +++ b/extensions/discord/src/monitor/native-command.ts @@ -243,7 +243,12 @@ function resolveDiscordGuildNativeCommandAuthorized(params: { configured: hasAccessRestrictions, allowed: memberAllowed, }; - const fallbackAuthorizers = [policyAuthorizer, ownerAuthorizer, memberAuthorizer]; + const hasStricterAccessRestrictions = ownerAuthorizer.configured || memberAuthorizer.configured; + const policyFallbackAuthorizer = { + configured: policyAuthorizer.configured && !hasStricterAccessRestrictions, + allowed: policyAuthorizer.allowed, + }; + const fallbackAuthorizers = [policyFallbackAuthorizer, ownerAuthorizer, memberAuthorizer]; return resolveCommandAuthorizedFromAuthorizers({ useAccessGroups: params.useAccessGroups, authorizers: params.useAccessGroups