openclaw - ✅(Solved) Fix [Bug]: Discord guild-admin actions execute without requester authorization checks [2 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#68703Fetched 2026-04-19 15:08:30
View on GitHub
Comments
0
Participants
1
Timeline
2
Reactions
0
Author
Participants
Timeline (top)
cross-referenced ×2

Error Message

throw new Error( throw new Error("Sender does not have required permissions for this moderation action."); throw new Error("Discord channel management is disabled.");

Root Cause

The root cause is a missing plugin-owned trusted-requester declaration plus missing runtime permission checks in the Discord guild-admin action path. The generic dispatcher in src/channels/plugins/message-action-dispatch.ts already supports privileged actions via plugin.actions.requiresTrustedRequesterSender(...), but the Discord adapter in extensions/discord/src/channel-actions.ts never declares that guild-admin mutations require a trusted requester.

Fix Action

Fix / Workaround

File: src/channels/plugins/message-action-dispatch.ts

export async function dispatchChannelMessageAction(
  ctx: ChannelMessageActionContext,
): Promise<AgentToolResult<unknown> | null> {
  if (requiresTrustedRequesterSender(ctx) && !ctx.requesterSenderId?.trim()) {
    throw new Error(
      `Trusted sender identity is required for ${ctx.channel}:${ctx.action} in tool-driven contexts.`,
    );
  }
  return await plugin.actions.handleAction(ctx);
}

The root cause is a missing plugin-owned trusted-requester declaration plus missing runtime permission checks in the Discord guild-admin action path. The generic dispatcher in src/channels/plugins/message-action-dispatch.ts already supports privileged actions via plugin.actions.requiresTrustedRequesterSender(...), but the Discord adapter in extensions/discord/src/channel-actions.ts never declares that guild-admin mutations require a trusted requester.

handleDiscordAction dispatches channelDelete (and equivalent guild mutations) to handleDiscordGuildAction in extensions/discord/src/actions/runtime.guild.ts. The only gate at that level is isActionEnabled(...), which controls feature enablement only. No requester permission validation equivalent to moderation's verifySenderModerationPermission runs before the bot-credentialed Discord API calls.

PR fix notes

PR #68705: fix: Discord guild-admin actions execute without requester...

Description (problem / solution / changelog)

Fix Summary

Discord guild-admin mutation actions (emoji-upload, sticker-upload, role-add, role-remove, channel-create, channel-edit, channel-delete, channel-move, category-create, category-edit, category-delete, event-create) bypass trusted-requester authorization entirely, allowing any Discord guild member who can message the agent to trigger bot-privileged guild mutations without holding the corresponding Discord permissions (MANAGE_CHANNELS, MANAGE_ROLES, guild-expression management, event-management permissions). The omission is confirmed by contrast with moderation actions, which are correctly gated via verifySenderModerationPermission, establishing that requester authorization was intended for bot-privileged actions but was never implemented for the guild-admin family.

Issue Linkage

Fixes #68703

Security Snapshot

  • CVSS v3.1: 7.1 (High)
  • CVSS v4.0: 7.1 (High)

Implementation Details

Files Changed

  • extensions/discord/src/actions/handle-action.guild-admin.ts (+22/-3)
  • extensions/discord/src/actions/handle-action.test.ts (+30/-0)
  • extensions/discord/src/actions/runtime.guild.authz.test.ts (+172/-0)
  • extensions/discord/src/actions/runtime.guild.ts (+84/-0)
  • extensions/discord/src/channel-actions.test.ts (+21/-0)
  • extensions/discord/src/channel-actions.ts (+20/-0)

Technical Analysis

The root cause is a missing plugin-owned trusted-requester declaration plus missing runtime permission checks in the Discord guild-admin action path. The generic dispatcher in src/channels/plugins/message-action-dispatch.ts already supports privileged actions via plugin.actions.requiresTrustedRequesterSender(...), but the Discord adapter in extensions/discord/src/channel-actions.ts never declares that guild-admin mutations require a trusted requester.

Execution proceeds to plugin.actions.handleAction(ctx), which for Discord routes through handleDiscordMessageActiontryHandleDiscordMessageActionGuildAdmin in extensions/discord/src/actions/handle-action.guild-admin.ts. This function extracts action parameters but never calls hasAnyGuildPermissionDiscord or any equivalent requester verification; it passes no senderUserId in the handleDiscordAction call for emoji/sticker, channel/category, role, or event mutations. senderUserId is only forwarded in the isDiscordModerationAction branch.

handleDiscordAction dispatches channelDelete (and equivalent guild mutations) to handleDiscordGuildAction in extensions/discord/src/actions/runtime.guild.ts. The only gate at that level is isActionEnabled(...), which controls feature enablement only. No requester permission validation equivalent to moderation's verifySenderModerationPermission runs before the bot-credentialed Discord API calls.

  1. Deploy OpenClaw with a Discord bot account that has MANAGE_CHANNELS permission in a guild. Leave channels.discord.actions.channels unset (defaults to true).
  2. From a regular Discord member account with no elevated guild permissions, send a message to the agent that causes it to invoke the message.channel-delete tool action with { channelId: "<target-channel-id>" }.
  3. The request reaches dispatchChannelMessageAction with action: "channel-delete". requiresTrustedRequesterSender(ctx) returns false because the Discord plugin provides no requiresTrustedRequesterSender hook for guild-admin actions.
  4. Execution flows: handleDiscordMessageActiontryHandleDiscordMessageActionGuildAdminhandleDiscordAction({ action: "channelDelete", channelId }, cfg)handleDiscordGuildAction("channelDelete", params, isActionEnabled).
  5. isActionEnabled("channels") returns true (default). deleteChannelDiscord(channelId) executes with bot credentials, destroying the channel.

The call chain with no requester authorization at any step:

dispatchChannelMessageAction({ channel:"discord", action:"channel-delete", params:{ channelId:"<id>" } })
  → requiresTrustedRequesterSender() → false (Discord plugin does not declare channel-delete as trusted-requester-only)
  → plugin.actions.handleAction(ctx)
  → handleDiscordMessageAction(...)
  → tryHandleDiscordMessageActionGuildAdmin(...)   [extensions/discord/src/actions/handle-action.guild-admin.ts]
  → handleDiscordAction({ action:"channelDelete", channelId }, cfg)
  → handleDiscordGuildAction("channelDelete", params, isActionEnabled)   [extensions/discord/src/actions/runtime.guild.ts]
  → isActionEnabled("channels") → true (default)
  → deleteChannelDiscord(channelId)   ← bot-credentialed destructive API call

Validation Evidence

  • Command: pnpm exec oxlint src/ && pnpm build && pnpm check && pnpm test
  • Status: passed (with pre-existing baseline failures)

Risk and Compatibility

  • non-breaking; no known regression impact

AI-Assisted Disclosure

  • AI-assisted: yes
  • Model: github-copilot/gpt-5.4

Changed files

  • .github/workflows/openclaw-live-and-e2e-checks-reusable.yml (modified, +6/-97)
  • .github/workflows/openclaw-release-checks.yml (modified, +2/-52)
  • .github/workflows/openclaw-scheduled-live-checks.yml (modified, +1/-46)
  • CHANGELOG.md (modified, +0/-5)
  • docs/.generated/config-baseline.sha256 (modified, +3/-3)
  • docs/.generated/plugin-sdk-api-baseline.sha256 (modified, +2/-2)
  • docs/concepts/agent-workspace.md (modified, +2/-2)
  • docs/concepts/context.md (modified, +2/-2)
  • docs/concepts/system-prompt.md (modified, +2/-2)
  • docs/gateway/configuration-reference.md (modified, +4/-4)
  • extensions/anthropic/provider-contract-api.ts (removed, +0/-59)
  • extensions/discord/directory-contract-api.ts (removed, +0/-4)
  • extensions/discord/src/actions/handle-action.guild-admin.ts (modified, +22/-3)
  • extensions/discord/src/actions/handle-action.test.ts (modified, +30/-0)
  • extensions/discord/src/actions/runtime.guild.authz.test.ts (added, +238/-0)
  • extensions/discord/src/actions/runtime.guild.ts (modified, +132/-0)
  • extensions/discord/src/actions/runtime.test.ts (modified, +34/-7)
  • extensions/discord/src/channel-actions.test.ts (modified, +21/-0)
  • extensions/discord/src/channel-actions.ts (modified, +20/-0)
  • extensions/discord/src/directory-config.ts (modified, +1/-1)
  • extensions/discord/src/monitor/native-command.plugin-dispatch.test.ts (modified, +43/-43)
  • extensions/discord/src/monitor/native-command.ts (modified, +12/-28)
  • extensions/discord/src/outbound-adapter.ts (modified, +11/-43)
  • extensions/fal/index.ts (modified, +32/-2)
  • extensions/fal/provider-contract-api.ts (removed, +0/-31)
  • extensions/fal/provider-registration.ts (removed, +0/-38)
  • extensions/feishu/src/card-action.ts (modified, +2/-5)
  • extensions/google/provider-contract-api.ts (removed, +0/-61)
  • extensions/matrix/src/matrix/client/create-client.test.ts (modified, +0/-55)
  • extensions/matrix/src/matrix/client/create-client.ts (modified, +2/-6)
  • extensions/minimax/provider-contract-api.ts (removed, +0/-84)
  • extensions/moonshot/provider-contract-api.ts (removed, +0/-33)
  • extensions/openai/openai.live.test.ts (modified, +1/-2)
  • extensions/openai/provider-contract-api.ts (removed, +0/-54)
  • extensions/openrouter/provider-contract-api.ts (removed, +0/-26)
  • extensions/qa-lab/src/lab-server.test.ts (modified, +26/-51)
  • extensions/qa-lab/src/run-config.test.ts (modified, +0/-12)
  • extensions/qa-lab/src/run-config.ts (modified, +4/-17)
  • extensions/slack/directory-contract-api.ts (removed, +0/-4)
  • extensions/slack/inbound-contract-test-api.ts (removed, +0/-2)
  • extensions/slack/outbound-payload-test-api.ts (removed, +0/-1)
  • extensions/slack/src/directory-config.ts (modified, +1/-1)
  • extensions/telegram/directory-contract-api.ts (removed, +0/-4)
  • extensions/telegram/src/account-config.ts (removed, +0/-37)
  • extensions/telegram/src/accounts.ts (modified, +38/-3)
  • extensions/telegram/src/bot-message-dispatch.test.ts (modified, +2/-732)
  • extensions/telegram/src/bot-message-dispatch.ts (modified, +464/-578)
  • extensions/telegram/src/bot.create-telegram-bot.test.ts (modified, +55/-22)
  • extensions/telegram/src/directory-config.ts (modified, +8/-25)
  • extensions/telegram/src/draft-stream.test-helpers.ts (modified, +0/-6)
  • extensions/telegram/src/draft-stream.test.ts (modified, +7/-15)
  • extensions/telegram/src/draft-stream.ts (modified, +39/-33)
  • extensions/twitch/index.test.ts (modified, +8/-1)
  • extensions/webhooks/src/http.ts (modified, +3/-1)
  • extensions/whatsapp/contract-api.ts (modified, +0/-4)
  • extensions/whatsapp/directory-contract-api.ts (removed, +0/-4)
  • extensions/whatsapp/outbound-payload-test-api.ts (removed, +0/-1)
  • extensions/whatsapp/src/account-config.ts (modified, +1/-40)
  • extensions/whatsapp/src/accounts.test.ts (modified, +0/-108)
  • extensions/whatsapp/src/accounts.ts (modified, +0/-2)
  • extensions/whatsapp/src/auto-reply.broadcast-groups.combined.test.ts (modified, +0/-51)
  • extensions/whatsapp/src/auto-reply.test-harness.ts (modified, +1/-17)
  • extensions/whatsapp/src/auto-reply.web-auto-reply.compresses-common-formats-jpeg-cap.test.ts (modified, +8/-2)
  • extensions/whatsapp/src/auto-reply.web-auto-reply.connection-and-logging.e2e.test.ts (modified, +0/-104)
  • extensions/whatsapp/src/auto-reply/config.runtime.ts (modified, +0/-1)
  • extensions/whatsapp/src/auto-reply/mentions.ts (modified, +1/-5)
  • extensions/whatsapp/src/auto-reply/monitor.ts (modified, +3/-38)
  • extensions/whatsapp/src/auto-reply/monitor/ack-reaction.test.ts (modified, +6/-6)
  • extensions/whatsapp/src/auto-reply/monitor/ack-reaction.ts (modified, +2/-3)
  • extensions/whatsapp/src/auto-reply/monitor/broadcast.ts (modified, +1/-6)
  • extensions/whatsapp/src/auto-reply/monitor/group-activation.test.ts (removed, +0/-175)
  • extensions/whatsapp/src/auto-reply/monitor/group-activation.ts (modified, +42/-54)
  • extensions/whatsapp/src/auto-reply/monitor/group-gating.ts (modified, +7/-23)
  • extensions/whatsapp/src/auto-reply/monitor/on-message.ts (modified, +2/-5)
  • extensions/whatsapp/src/auto-reply/monitor/process-message.ts (modified, +85/-30)
  • extensions/whatsapp/src/auto-reply/types.ts (modified, +6/-2)
  • extensions/whatsapp/src/auto-reply/web-auto-reply-monitor.test.ts (modified, +21/-183)
  • extensions/whatsapp/src/auto-reply/web-auto-reply-utils.test.ts (modified, +1/-16)
  • extensions/whatsapp/src/channel.setup.test.ts (modified, +0/-123)
  • extensions/whatsapp/src/directory-config.ts (modified, +7/-16)
  • extensions/whatsapp/src/group-session-key.test.ts (removed, +0/-53)
  • extensions/whatsapp/src/group-session-key.ts (removed, +0/-41)
  • extensions/whatsapp/src/inbound-policy.ts (removed, +0/-196)
  • extensions/whatsapp/src/inbound/access-control.test.ts (modified, +8/-103)
  • extensions/whatsapp/src/inbound/access-control.ts (modified, +77/-64)
  • extensions/whatsapp/src/inbound/monitor.ts (modified, +22/-43)
  • extensions/whatsapp/src/outbound-adapter.ts (modified, +3/-4)
  • extensions/whatsapp/src/pairing-security.test-harness.ts (modified, +9/-5)
  • extensions/whatsapp/src/setup-finalize.ts (modified, +11/-55)
  • extensions/whatsapp/src/setup-test-helpers.ts (modified, +0/-9)
  • extensions/whatsapp/src/shared.ts (modified, +21/-91)
  • extensions/whatsapp/src/test-helpers.ts (modified, +0/-19)
  • extensions/xai/provider-contract-api.ts (removed, +0/-22)
  • extensions/xai/web-search.test.ts (modified, +2/-2)
  • extensions/zalo/src/group-access.ts (modified, +3/-1)
  • package.json (modified, +0/-5)
  • scripts/check-plugin-sdk-exports.mjs (modified, +1/-33)
  • scripts/lib/live-docker-auth.sh (modified, +2/-62)
  • scripts/lib/plugin-sdk-entrypoints.json (modified, +0/-1)
  • scripts/prepare-codex-ci-config.ts (removed, +0/-51)

PR #68716: fix: Discord guild-admin actions execute without requester...

Description (problem / solution / changelog)

Summary

  • Problem: Discord guild-admin message actions could run with bot privileges without verifying that the requesting guild member held the corresponding Discord permissions.
  • Why it matters: any guild member who could message the agent could trigger privileged channel, role, emoji, sticker, or event mutations through the bot.
  • What changed: this PR requires trusted requester identity for guild-admin actions, forwards senderUserId into the guild-admin runtime, fails closed when sender identity is missing for privileged runtime calls, preserves disabled-action gating before permission lookups, and adds regression coverage for direct runtime permission writes.
  • What did NOT change (scope boundary): no unrelated channel/plugin/runtime cleanup from the previously closed dirty PR is included here.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #68703
  • Related #68705
  • This PR fixes a bug or regression

Root Cause (if applicable)

  • Root cause: the Discord plugin never marked guild-admin message actions as trusted-requester-only, and the guild-admin runtime path did not enforce requester permission checks analogous to moderation actions.
  • Missing detection / guardrail: there was no regression coverage for guild-admin requester authz or direct runtime privileged actions such as channelPermissionSet.
  • Contributing context (if known): the first PR attempt was later auto-closed as dirty after an unrelated large follow-up commit landed on the branch; this PR is the clean replacement limited to the Discord fix.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: extensions/discord/src/actions/runtime.guild.authz.test.ts, extensions/discord/src/actions/runtime.test.ts, extensions/discord/src/actions/handle-action.test.ts, extensions/discord/src/channel-actions.test.ts
  • Scenario the test should lock in: privileged guild-admin actions require sender identity and matching Discord permissions, disabled channel actions fail before permission lookups, and direct runtime permission writes are covered.
  • Why this is the smallest reliable guardrail: the bug lives entirely in Discord message-action dispatch and guild runtime authorization logic.
  • Existing test that already covers this (if any): none for the full guild-admin requester-authz path before this fix.
  • If no new test is added, why not: N/A

User-visible / Behavior Changes

  • Discord guild-admin actions now reject untrusted or unauthorized requesters instead of executing with bot privileges.
  • Direct privileged guild runtime calls now fail closed when senderUserId is missing.
  • Disabled channel-management actions now return the disabled error before doing Discord permission lookups.

Diagram (if applicable)

Before:
[guild member message] -> [guild-admin action dispatch] -> [bot credential mutation]

After:
[guild member message] -> [trusted requester gate] -> [runtime permission check] -> [allowed mutation or authz error]

Security Impact (required)

  • New permissions/capabilities? (Yes/No) No
  • Secrets/tokens handling changed? (Yes/No) No
  • New/changed network calls? (Yes/No) No
  • Command/tool execution surface changed? (Yes/No) Yes
  • Data access scope changed? (Yes/No) No
  • If any Yes, explain risk + mitigation: privileged Discord guild-admin actions now require requester identity and matching Discord permissions before execution; regression tests cover the fail-closed and disabled-gate behaviors.

Repro + Verification

Environment

  • OS: macOS
  • Runtime/container: Node 22 + pnpm
  • Model/provider: N/A
  • Integration/channel (if any): Discord
  • Relevant config (redacted): Discord bot with guild-admin capabilities; default channel actions enabled

Steps

  1. Configure a Discord bot with guild-admin permissions and allow a regular guild member to message the agent.
  2. Invoke a guild-admin message action such as channel-delete from that regular guild member.
  3. Observe authorization behavior before and after the fix.

Expected

  • Unauthorized or unidentified requesters are rejected before privileged Discord mutations run.

Actual

  • Before the fix, guild-admin actions could execute with bot credentials without equivalent requester authorization checks.

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

  • Verified scenarios: reran focused Discord tests for guild-admin authz, guild runtime behavior, and message-action dispatch; built the repo successfully on the clean replacement branch.
  • Edge cases checked: missing senderUserId, disabled channel-management actions, direct channelPermissionSet runtime path, account-scoped channel re-enable path.
  • What you did not verify: live Discord manual repro on a deployed bot.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? (Yes/No) Yes
  • Config/env changes? (Yes/No) No
  • Migration needed? (Yes/No) No
  • If yes, exact upgrade steps: N/A

Risks and Mitigations

  • Risk: privileged direct runtime callers without senderUserId will now fail closed.
    • Mitigation: this is the intended security posture, and tests cover the new behavior explicitly.

Changed files

  • extensions/discord/src/actions/handle-action.guild-admin.ts (modified, +22/-3)
  • extensions/discord/src/actions/handle-action.test.ts (modified, +30/-0)
  • extensions/discord/src/actions/runtime.guild.authz.test.ts (added, +238/-0)
  • extensions/discord/src/actions/runtime.guild.ts (modified, +132/-0)
  • extensions/discord/src/actions/runtime.test.ts (modified, +34/-7)
  • extensions/discord/src/channel-actions.test.ts (modified, +21/-0)
  • extensions/discord/src/channel-actions.ts (modified, +20/-0)

Code Example

export const discordMessageActions: ChannelMessageActionAdapter = {
  describeMessageTool: describeDiscordMessageTool,
  // No requiresTrustedRequesterSender hook is declared for guild-admin actions,
  // so tool-driven requests do not require a trusted requester identity.
  handleAction: async (ctx) => await loadDiscordChannelActionsRuntime().handleDiscordMessageAction(ctx),
};

---

export async function dispatchChannelMessageAction(
  ctx: ChannelMessageActionContext,
): Promise<AgentToolResult<unknown> | null> {
  if (requiresTrustedRequesterSender(ctx) && !ctx.requesterSenderId?.trim()) {
    throw new Error(
      `Trusted sender identity is required for ${ctx.channel}:${ctx.action} in tool-driven contexts.`,
    );
  }
  return await plugin.actions.handleAction(ctx);
}

---

if (action === "channel-delete") {
  const channelId = readStringParam(actionParams, "channelId", {
    required: true,
  });
  return await handleDiscordAction(
    { action: "channelDelete", accountId: accountId ?? undefined, channelId },
    cfg,
  );
}

// The guild-admin branches never forward ctx.requesterSenderId as senderUserId.

---

async function verifySenderModerationPermission(params: {
  guildId: string;
  senderUserId?: string;
  requiredPermission: bigint;
  accountId?: string;
}) {
  if (!params.senderUserId) { return; }
  const hasPermission = await hasAnyGuildPermissionDiscord(
    params.guildId, params.senderUserId, [params.requiredPermission],
    params.accountId ? { accountId: params.accountId } : undefined,
  );
  if (!hasPermission) {
    throw new Error("Sender does not have required permissions for this moderation action.");
  }
}

---

export async function handleDiscordGuildAction(
  action: string,
  params: Record<string, unknown>,
  isActionEnabled: ActionGate<DiscordActionConfig>,
): Promise<AgentToolResult<unknown>> {
  const accountId = readStringParam(params, "accountId");
  // No senderUserId/requester permission validation runs here before bot-credentialed guild mutations.
  switch (action) {
    case "channelDelete":
      if (!isActionEnabled("channels")) {
        throw new Error("Discord channel management is disabled.");
      }
      return jsonResult(await deleteChannelDiscord(channelId, { accountId }));
  }
}

---

dispatchChannelMessageAction({ channel:"discord", action:"channel-delete", params:{ channelId:"<id>" } })
requiresTrustedRequesterSender()false (Discord plugin does not declare channel-delete as trusted-requester-only)
  → plugin.actions.handleAction(ctx)
handleDiscordMessageAction(...)
tryHandleDiscordMessageActionGuildAdmin(...)   [extensions/discord/src/actions/handle-action.guild-admin.ts]
handleDiscordAction({ action:"channelDelete", channelId }, cfg)
handleDiscordGuildAction("channelDelete", params, isActionEnabled)   [extensions/discord/src/actions/runtime.guild.ts]
isActionEnabled("channels")true (default)
deleteChannelDiscord(channelId)   ← bot-credentialed destructive API call
RAW_BUFFERClick to expand / collapse

Severity Assessment

CVSS Assessment

Metricv3.1v4.0
Score7.1 / 10.07.1 / 10.0
SeverityHighHigh
VectorCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:LCVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:H/VA:L/SC:N/SI:N/SA:N
CalculatorCVSS v3.1 CalculatorCVSS v4.0 Calculator

Threat Model Alignment

Classification: security-specific

This finding crosses a concrete authorization boundary within OpenClaw's Discord integration: the application deliberately implements per-requester Discord permission verification for moderation actions (timeout, kick, ban) via verifySenderModerationPermission + hasAnyGuildPermissionDiscord in discord-actions-moderation.ts, but the identical protection was never applied to guild-admin mutation actions (channel-create, channel-delete, channel-edit, channel-move, category-create, category-edit, category-delete, role-add, role-remove). A Discord guild member who can message the agent is not the same as an authenticated gateway operator, and the pattern of enforcing Discord permission checks for moderation but omitting them for guild-admin actions constitutes a real authorization bypass — any guild member can drive the bot's elevated credentials to destroy channels or modify roles they lack permission to touch natively. This is not covered by the Out of Scope provisions for multi-tenant gateway isolation: the claim is about missing application-level requester authorization within the Discord access control layer, not about session isolation or operator-level gateway access.

Impact

Discord guild-admin mutation actions (emoji-upload, sticker-upload, role-add, role-remove, channel-create, channel-edit, channel-delete, channel-move, category-create, category-edit, category-delete, event-create) bypass trusted-requester authorization entirely, allowing any Discord guild member who can message the agent to trigger bot-privileged guild mutations without holding the corresponding Discord permissions (MANAGE_CHANNELS, MANAGE_ROLES, guild-expression management, event-management permissions). The omission is confirmed by contrast with moderation actions, which are correctly gated via verifySenderModerationPermission, establishing that requester authorization was intended for bot-privileged actions but was never implemented for the guild-admin family.

Affected Component

File: extensions/discord/src/channel-actions.ts

export const discordMessageActions: ChannelMessageActionAdapter = {
  describeMessageTool: describeDiscordMessageTool,
  // No requiresTrustedRequesterSender hook is declared for guild-admin actions,
  // so tool-driven requests do not require a trusted requester identity.
  handleAction: async (ctx) => await loadDiscordChannelActionsRuntime().handleDiscordMessageAction(ctx),
};

File: src/channels/plugins/message-action-dispatch.ts

export async function dispatchChannelMessageAction(
  ctx: ChannelMessageActionContext,
): Promise<AgentToolResult<unknown> | null> {
  if (requiresTrustedRequesterSender(ctx) && !ctx.requesterSenderId?.trim()) {
    throw new Error(
      `Trusted sender identity is required for ${ctx.channel}:${ctx.action} in tool-driven contexts.`,
    );
  }
  return await plugin.actions.handleAction(ctx);
}

File: extensions/discord/src/actions/handle-action.guild-admin.ts

if (action === "channel-delete") {
  const channelId = readStringParam(actionParams, "channelId", {
    required: true,
  });
  return await handleDiscordAction(
    { action: "channelDelete", accountId: accountId ?? undefined, channelId },
    cfg,
  );
}

// The guild-admin branches never forward ctx.requesterSenderId as senderUserId.

File: extensions/discord/src/actions/runtime.moderation.ts (existing protection for moderation, absent for guild-admin)

async function verifySenderModerationPermission(params: {
  guildId: string;
  senderUserId?: string;
  requiredPermission: bigint;
  accountId?: string;
}) {
  if (!params.senderUserId) { return; }
  const hasPermission = await hasAnyGuildPermissionDiscord(
    params.guildId, params.senderUserId, [params.requiredPermission],
    params.accountId ? { accountId: params.accountId } : undefined,
  );
  if (!hasPermission) {
    throw new Error("Sender does not have required permissions for this moderation action.");
  }
}

File: extensions/discord/src/actions/runtime.guild.ts

export async function handleDiscordGuildAction(
  action: string,
  params: Record<string, unknown>,
  isActionEnabled: ActionGate<DiscordActionConfig>,
): Promise<AgentToolResult<unknown>> {
  const accountId = readStringParam(params, "accountId");
  // No senderUserId/requester permission validation runs here before bot-credentialed guild mutations.
  switch (action) {
    case "channelDelete":
      if (!isActionEnabled("channels")) {
        throw new Error("Discord channel management is disabled.");
      }
      return jsonResult(await deleteChannelDiscord(channelId, { accountId }));
  }
}

Technical Reproduction

The root cause is a missing plugin-owned trusted-requester declaration plus missing runtime permission checks in the Discord guild-admin action path. The generic dispatcher in src/channels/plugins/message-action-dispatch.ts already supports privileged actions via plugin.actions.requiresTrustedRequesterSender(...), but the Discord adapter in extensions/discord/src/channel-actions.ts never declares that guild-admin mutations require a trusted requester.

Execution proceeds to plugin.actions.handleAction(ctx), which for Discord routes through handleDiscordMessageActiontryHandleDiscordMessageActionGuildAdmin in extensions/discord/src/actions/handle-action.guild-admin.ts. This function extracts action parameters but never calls hasAnyGuildPermissionDiscord or any equivalent requester verification; it passes no senderUserId in the handleDiscordAction call for emoji/sticker, channel/category, role, or event mutations. senderUserId is only forwarded in the isDiscordModerationAction branch.

handleDiscordAction dispatches channelDelete (and equivalent guild mutations) to handleDiscordGuildAction in extensions/discord/src/actions/runtime.guild.ts. The only gate at that level is isActionEnabled(...), which controls feature enablement only. No requester permission validation equivalent to moderation's verifySenderModerationPermission runs before the bot-credentialed Discord API calls.

  1. Deploy OpenClaw with a Discord bot account that has MANAGE_CHANNELS permission in a guild. Leave channels.discord.actions.channels unset (defaults to true).
  2. From a regular Discord member account with no elevated guild permissions, send a message to the agent that causes it to invoke the message.channel-delete tool action with { channelId: "<target-channel-id>" }.
  3. The request reaches dispatchChannelMessageAction with action: "channel-delete". requiresTrustedRequesterSender(ctx) returns false because the Discord plugin provides no requiresTrustedRequesterSender hook for guild-admin actions.
  4. Execution flows: handleDiscordMessageActiontryHandleDiscordMessageActionGuildAdminhandleDiscordAction({ action: "channelDelete", channelId }, cfg)handleDiscordGuildAction("channelDelete", params, isActionEnabled).
  5. isActionEnabled("channels") returns true (default). deleteChannelDiscord(channelId) executes with bot credentials, destroying the channel.

The call chain with no requester authorization at any step:

dispatchChannelMessageAction({ channel:"discord", action:"channel-delete", params:{ channelId:"<id>" } })
  → requiresTrustedRequesterSender() → false (Discord plugin does not declare channel-delete as trusted-requester-only)
  → plugin.actions.handleAction(ctx)
  → handleDiscordMessageAction(...)
  → tryHandleDiscordMessageActionGuildAdmin(...)   [extensions/discord/src/actions/handle-action.guild-admin.ts]
  → handleDiscordAction({ action:"channelDelete", channelId }, cfg)
  → handleDiscordGuildAction("channelDelete", params, isActionEnabled)   [extensions/discord/src/actions/runtime.guild.ts]
  → isActionEnabled("channels") → true (default)
  → deleteChannelDiscord(channelId)   ← bot-credentialed destructive API call

Demonstrated Impact

  • Discord plugin trusted-requester hook: extensions/discord/src/channel-actions.ts exposes describeMessageTool and handleAction, but does not declare requiresTrustedRequesterSender(...) for guild-admin mutations. The generic dispatcher therefore never requires ctx.requesterSenderId for those actions.
  • Moderation protection exists only for moderation: extensions/discord/src/actions/runtime.moderation.ts enforces hasAnyGuildPermissionDiscord(...) before timeout, kick, and ban. No equivalent function exists in extensions/discord/src/actions/runtime.guild.ts.
  • requesterSenderId forwarding gap: extensions/discord/src/actions/handle-action.guild-admin.ts forwards senderUserId only for the moderation branch. Emoji/sticker uploads, channel/category mutations, role mutations, and event creation never forward ctx.requesterSenderId into the runtime.
  • Runtime gate is feature-only: extensions/discord/src/actions/runtime.guild.ts gates on isActionEnabled(...) only. That controls account feature enablement, not whether the Discord requester actually has MANAGE_CHANNELS, MANAGE_ROLES, or equivalent permissions.
  • The existing security coverage in src/channels/plugins/message-actions.security.test.ts exercises only the trusted-requester moderation path and leaves the Discord guild-admin action family uncovered.

Environment

Verified against release tag v2026.4.15 (published 2026-04-16T21:50:22Z), commit 041266a6699cac3baef8ef39db41fa26f29f9db3. Requires a deployed OpenClaw Discord bot instance with MANAGE_CHANNELS or MANAGE_ROLES permission in a guild, with channels.discord.actions.channels unset or set to true (default). A Discord guild member with no elevated permissions and the ability to message the agent is the attacker prerequisite.

Remediation Advice

Declare Discord guild-admin mutation actions as trusted-requester-only in the Discord channel adapter so tool-driven invocations require requesterSenderId. Then implement a verifySenderGuildAdminPermission function in extensions/discord/src/actions/runtime.guild.ts, analogous to moderation's verifySenderModerationPermission, requiring the forwarded senderUserId to hold the relevant Discord permission bits (MANAGE_CHANNELS, MANAGE_ROLES, guild-expression management, event-management permissions) via hasAnyGuildPermissionDiscord(...) before any bot-credentialed guild mutation executes. Thread senderUserId from ChannelMessageActionContext.requesterSenderId through handle-action.guild-admin.ts into handleDiscordAction for every destructive guild-admin path.

<!-- submission-marker:AA-uig-discord-guild-admin-actions-missing-requester-authz -->

extent analysis

TL;DR

To fix the authorization bypass vulnerability in Discord guild-admin actions, declare these actions as trusted-requester-only and implement permission checks using a verifySenderGuildAdminPermission function.

Guidance

  1. Declare trusted-requester requirement: In extensions/discord/src/channel-actions.ts, add a requiresTrustedRequesterSender hook for guild-admin mutations to ensure ctx.requesterSenderId is required for these actions.
  2. Implement permission verification: Create a verifySenderGuildAdminPermission function in extensions/discord/src/actions/runtime.guild.ts, similar to verifySenderModerationPermission, to check if the senderUserId has the necessary Discord permissions before executing bot-credentialed guild mutations.
  3. Forward senderUserId: Modify extensions/discord/src/actions/handle-action.guild-admin.ts to forward ctx.requesterSenderId as senderUserId for all guild-admin actions, not just moderation actions.
  4. Integrate permission checks: Call verifySenderGuildAdminPermission before executing any bot-credentialed guild mutation in handleDiscordGuildAction to ensure the requester has the required permissions.

Example

// extensions/discord/src/actions/runtime.guild.ts
async function verifySenderGuildAdminPermission(params: {
  guildId: string;
  senderUserId?: string;
  requiredPermission: bigint;
  accountId?: string;
}) {
  if (!params.senderUserId) { return; }
  const hasPermission = await hasAnyGuildPermissionDiscord(
    params.guildId, params.senderUserId, [params.requiredPermission],
    params.accountId ? { accountId: params.accountId } : undefined,
  );
  if (!hasPermission) {
    throw new Error("Sender does not have required permissions for this guild-admin action.");
  }
}

Notes

  • This fix assumes the existence of a hasAnyGuildPermissionDiscord function that can check if a user has specific Discord permissions.
  • The verifySenderGuildAdminPermission function should be called for each guild-admin action before executing the corresponding bot-credentialed API call.

Recommendation

Apply the workaround by declaring guild-admin actions as trusted-requester-only and implementing the verifySenderGuildAdminPermission function to ensure that only authorized users can perform these actions. This will prevent unauthorized Discord guild members from driving the bot's elevated credentials to destroy channels or modify roles.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING