security: gate /help, signature modal submit, and cancel_delete_tag on staff role

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
This commit is contained in:
2026-05-19 19:58:41 +00:00
parent e8e114e4ad
commit c79463fc2a
3 changed files with 22 additions and 8 deletions

View File

@@ -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');

View File

@@ -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);

View File

@@ -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);
}