Message context command bypasses member permission gate for guild expression creation #224

Open
opened 2026-07-08 03:21:34 +00:00 by stationedK-06 · 2 comments
stationedK-06 commented 2026-07-08 03:21:34 +00:00 (Migrated from codeberg.org)

Description

Category: Authorization bypass / privileged action confused deputy
CWE: CWE-862, CWE-863 (Check CWE; it is actually helpful)

Brief Summary

Steal Emotes/Stickers message context command is registered for guild use but never sets ManageGuildExpressions or performs an equivalent runtime member permission check before calling guild.emojis.create() and guild.stickers.create(). Thus, a normal guild member can exercise bot-owned expression management authority if the bot has that permission.

The root causes of this issue are as stated below (may be there are more)

  • src/contextCommands/steal.ts:18-22: guild context command lacks permission default
const command: MessageContextCommand = {
    data: new ContextMenuCommandBuilder()
        .setName("Steal Emotes/Stickers")
        .setType(ApplicationCommandType.Message)
        .setContexts(InteractionContextType.Guild),
  • src/contextCommands/steal.ts:102-106: Command performs the privileged guild expression creation side effect using bot authority.
const emoji = await guild.emojis.create({
                    attachment: imageAttachment.url,
                    name,
                });
  • src/contextCommands/steal.ts:164-170: privileged guild state change reached from the same unguarded context command
const created = await guild.stickers.create({
    file: {
        attachment: Buffer.from(await stickerRes.arrayBuffer()),
        name: `sticker.${ext}`,
    },
    name: sticker.name,
    tags: sticker.tags ?? sticker.name,
});
  • src/events/interactionCreate:64-80: Dispatcher invokes context commands without local permission check
    • dispatcher looks up the command name and calls execute() and this does not enforce ManageGuildExpressions at runtime
const command = client.contextCommands.get(interaction.commandName);
if (!command) {
    logger.warn(
        `No context command matching "${interaction.commandName}" in memory (loaded: ${[...client.contextCommands.keys()].join(", ")})`,
    );
    return;
}

try {
    if (interaction.isMessageContextMenuCommand()) {
        await (command as MessageContextCommand).execute(
            interaction as MessageContextMenuCommandInteraction,
        );
    } else {
        await (command as UserContextCommand).execute(
            interaction as UserContextMenuCommandInteraction,
        );
    }

Suggested remediations are as stated below

  1. add .setDefaultMemberPermissions(PermissionFlagsBits.ManageuildExpressions) to the context command and also check it
  2. add `interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuildExpressions) before creating emojis, stickers, or sounds. Additionally, return an ephemeral denial if the check fails.
  3. May need additional changes since privileged Discord side effects are not behind both command registration defaults and runtime permission checks.
Also worth adding:

The slash /steal command does set .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuildExpressions), so it is better protected at the command-registration layer than the context command. However, it still lacks a runtime permission check before using privileged guild expression APIs. Both the slash command and the context command should check interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuildExpressions) before creating emojis, stickers, or soundboard sounds

### Description Category: Authorization bypass / privileged action confused deputy CWE: CWE-862, CWE-863 (Check CWE; it is actually helpful) ### Brief Summary Steal Emotes/Stickers message context command is registered for guild use but never sets ManageGuildExpressions or performs an equivalent runtime member permission check before calling `guild.emojis.create()` and `guild.stickers.create()`. Thus, a normal guild member can exercise bot-owned expression management authority if the bot has that permission. ### The root causes of this issue are as stated below (may be there are more) - `src/contextCommands/steal.ts:18-22`: guild context command lacks permission default ```ts const command: MessageContextCommand = { data: new ContextMenuCommandBuilder() .setName("Steal Emotes/Stickers") .setType(ApplicationCommandType.Message) .setContexts(InteractionContextType.Guild), ``` - `src/contextCommands/steal.ts:102-106`: Command performs the privileged guild expression creation side effect using bot authority. ```ts const emoji = await guild.emojis.create({ attachment: imageAttachment.url, name, }); ``` - `src/contextCommands/steal.ts:164-170`: privileged guild state change reached from the same unguarded context command ```ts const created = await guild.stickers.create({ file: { attachment: Buffer.from(await stickerRes.arrayBuffer()), name: `sticker.${ext}`, }, name: sticker.name, tags: sticker.tags ?? sticker.name, }); ``` - `src/events/interactionCreate:64-80`: Dispatcher invokes context commands without local permission check - dispatcher looks up the command name and calls `execute()` and this does not enforce `ManageGuildExpressions` at runtime ```ts const command = client.contextCommands.get(interaction.commandName); if (!command) { logger.warn( `No context command matching "${interaction.commandName}" in memory (loaded: ${[...client.contextCommands.keys()].join(", ")})`, ); return; } try { if (interaction.isMessageContextMenuCommand()) { await (command as MessageContextCommand).execute( interaction as MessageContextMenuCommandInteraction, ); } else { await (command as UserContextCommand).execute( interaction as UserContextMenuCommandInteraction, ); } ``` ### Suggested remediations are as stated below 1. add `.setDefaultMemberPermissions(PermissionFlagsBits.ManageuildExpressions)` to the context command and also check it 2. add `interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuildExpressions) before creating emojis, stickers, or sounds. Additionally, return an ephemeral denial if the check fails. 3. May need additional changes since privileged Discord side effects are not behind both command registration defaults and runtime permission checks. ##### Also worth adding: The slash /steal command does set `.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuildExpressions)`, so it is better protected at the command-registration layer than the context command. However, it still lacks a runtime permission check before using privileged guild expression APIs. Both the slash command and the context command should check `interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuildExpressions)` before creating emojis, stickers, or soundboard sounds
TenType commented 2026-07-11 05:51:23 +00:00 (Migrated from codeberg.org)

The slash /steal command does set .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuildExpressions), so it is better protected at the command-registration layer than the context command. However, it still lacks a runtime permission check before using privileged guild expression APIs. Both the slash command and the context command should check interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuildExpressions) before creating emojis, stickers, or soundboard sounds

I'm pretty sure Discord handles this, so there's no way for a user to bypass the check

> The slash /steal command does set .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuildExpressions), so it is better protected at the command-registration layer than the context command. However, it still lacks a runtime permission check before using privileged guild expression APIs. Both the slash command and the context command should check interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuildExpressions) before creating emojis, stickers, or soundboard sounds I'm pretty sure Discord handles this, so there's no way for a user to bypass the check
stationedK-06 commented 2026-07-11 06:15:22 +00:00 (Migrated from codeberg.org)

@TenType wrote in https://codeberg.org/ScottyLabs/dalmatian/issues/224#issuecomment-18993857:

The slash /steal command does set .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuildExpressions), so it is better protected at the command-registration layer than the context command. However, it still lacks a runtime permission check before using privileged guild expression APIs. Both the slash command and the context command should check interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuildExpressions) before creating emojis, stickers, or soundboard sounds

I'm pretty sure Discord handles this, so there's no way for a user to bypass the check

I don't think Discord fully covers this, though. We should separate this into two parts.
The first part is that the context command has no restriction for Discord to enforce. Steal never calls setDefaultMemberPermissions(), so by default all members can use any command unless we explicitly set a restriction. So there is actually nothing gating invocation here which means, distinct from the slash command issue, this is an actual bug so aka discord can't handle this.

The second part is about where the default is set. Default member permissions only control who can invoke the command, iirc, and that is adjustable per server (a server can grant a command to additional roles via server setting overrides of the Dalmatian setting, maybe? I'm not sure about Discord server settings). Anyways, the code itself never checks that the invoking member is authorized before using the bot's own privilege. And, trivially, these are independent controls.

@TenType wrote in https://codeberg.org/ScottyLabs/dalmatian/issues/224#issuecomment-18993857: > > The slash /steal command does set .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuildExpressions), so it is better protected at the command-registration layer than the context command. However, it still lacks a runtime permission check before using privileged guild expression APIs. Both the slash command and the context command should check interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuildExpressions) before creating emojis, stickers, or soundboard sounds > > I'm pretty sure Discord handles this, so there's no way for a user to bypass the check I don't think Discord fully covers this, though. We should separate this into two parts. The first part is that the context command has no restriction for Discord to enforce. Steal never calls setDefaultMemberPermissions(), so by default all members can use any command unless we explicitly set a restriction. So there is actually nothing gating invocation here which means, distinct from the slash command issue, this is an actual bug so aka discord can't handle this. The second part is about where the default is set. Default member permissions only control who can invoke the command, iirc, and that is adjustable per server (a server can grant a command to additional roles via server setting overrides of the Dalmatian setting, maybe? I'm not sure about Discord server settings). Anyways, the code itself never checks that the invoking member is authorized before using the bot's own privilege. And, trivially, these are independent controls.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
ScottyLabs/dalmatian#224
No description provided.