From c79463fc2a8b487a3118a4d6ee7d9558453d1d50 Mon Sep 17 00:00:00 2001 From: indifferentketchup Date: Tue, 19 May 2026 19:58:41 +0000 Subject: [PATCH] security: gate /help, signature modal submit, and cancel_delete_tag on staff role MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the remaining non-broccolini interaction paths after the prior TICKET_BUTTON_HANDLERS gate. After this commit, every bot interaction is staff-only except the panel buttons (open_ticket / open_ticket_thread / open_ticket_channel) and their ticket-creation modal submit — those have to stay public because they're how members and customers open tickets. Specific changes: - handlers/commands/index.js: handleCommand no longer has the `!== 'help'` carve-out. /help now goes through requireStaffRole like every other slash command. Non-staff get the same ephemeral "only available to the support team" reply. - broccolini-discord.js: the signature_modal_* modal-submit handler now calls requireStaffRole before writing to StaffSignature. /signature already gates the modal display via the slash-command staff check; this is defense in depth against directly crafted submissions. - handlers/buttons.js: cancel_delete_tag moved out of FREE_BUTTON_HANDLERS and gated alongside confirm_delete_tag::*. The dialog is only shown ephemerally to the staff who triggered /response delete, so non-staff can't reach it in normal flow; gating keeps the button surface consistent. Kept public (by design — these are the customer entry points): open_ticket / open_ticket_thread / open_ticket_channel buttons ticket_modal / ticket_modal_thread / ticket_modal_channel submits --- broccolini-discord.js | 4 ++++ handlers/buttons.js | 20 +++++++++++++++----- handlers/commands/index.js | 6 +++--- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/broccolini-discord.js b/broccolini-discord.js index e000e93..4cbd02f 100644 --- a/broccolini-discord.js +++ b/broccolini-discord.js @@ -11,6 +11,7 @@ const { mongoose } = require('./db-connection'); // Handlers const { handleButton, handleTicketModal } = require('./handlers/buttons'); const { handleCommand, handleContextMenu, handleAutocomplete } = require('./handlers/commands'); +const { requireStaffRole } = require('./handlers/commands/helpers'); const { handleDiscordReply } = require('./handlers/messages'); // Services & jobs @@ -110,6 +111,9 @@ client.on('interactionCreate', async interaction => { } if (interaction.isModalSubmit() && interaction.customId.startsWith('signature_modal_')) { + // Staff-only: /signature shows this modal, which is gated; double-gate the + // submit path in case an attacker crafts the submission directly. + if (await requireStaffRole(interaction)) return; // Handle signature modal submit try { const valediction = interaction.fields.getTextInputValue('valediction'); diff --git a/handlers/buttons.js b/handlers/buttons.js index a597cee..fda8d95 100644 --- a/handlers/buttons.js +++ b/handlers/buttons.js @@ -707,12 +707,15 @@ async function postTicketWelcomeEmbeds(channel, interaction, email, game, descri // Dispatch tables // ============================================================ -/** Buttons that don't depend on a ticket-bound channel. */ +/** + * Public-facing buttons that don't require a staff role: the panel buttons + * that any member uses to open a ticket. Customer-facing entry points stay + * here. cancel_delete_tag is staff-only and gated separately in handleButton. + */ const FREE_BUTTON_HANDLERS = { open_ticket: handleOpenTicketModal, open_ticket_thread: handleOpenTicketModal, - open_ticket_channel: handleOpenTicketModal, - cancel_delete_tag: handleTagDeleteCancel + open_ticket_channel: handleOpenTicketModal }; /** Buttons that fire inside a ticket channel. The dispatcher does the lookup. */ @@ -739,8 +742,15 @@ async function handleButton(interaction) { return handleTagDeleteConfirm(interaction); } - // FREE_BUTTON_HANDLERS are public-facing: open_ticket* (panel buttons, - // anyone can open a ticket) and cancel_delete_tag (no-op cancel). + // Tag-delete cancel: paired with the staff-only delete flow; gate to keep + // the button surface consistent (non-staff can't reach the dialog anyway). + if (customId === 'cancel_delete_tag') { + if (await requireStaffRole(interaction)) return; + return handleTagDeleteCancel(interaction); + } + + // FREE_BUTTON_HANDLERS are the public-facing panel buttons (open_ticket*). + // Customers/members must be able to click these to open a ticket. const freeHandler = FREE_BUTTON_HANDLERS[customId]; if (freeHandler) return freeHandler(interaction); diff --git a/handlers/commands/index.js b/handlers/commands/index.js index 3743681..5546932 100644 --- a/handlers/commands/index.js +++ b/handlers/commands/index.js @@ -274,11 +274,11 @@ const CONTEXT_MENU_HANDLERS = { }; /** - * Slash-command dispatcher. /help is open to everyone; everything else - * requires the staff role. + * Slash-command dispatcher. Every command is staff-only — including /help, + * which previously bypassed the role check. */ async function handleCommand(interaction) { - if (interaction.commandName !== 'help' && (await requireStaffRole(interaction))) return; + if (await requireStaffRole(interaction)) return; const handler = COMMAND_HANDLERS[interaction.commandName]; if (handler) await handler(interaction); }