security: gate ticket buttons + tag-delete confirm on staff role
handleButton routed claim_ticket, close_ticket, confirm_close /
confirm_close_with_email / confirm_close_no_email, cancel_close,
escalate_ticket, escalate_to_tier2, escalate_to_tier3, deescalate_ticket,
and confirm_delete_tag::* straight to their handlers without any staff
check. Any non-staff member with View Channel on a ticket — the ticket
creator themselves, or anyone /add'd to it — could click those buttons
and mutate ticket state (claim, escalate, close, delete saved-response
tags).
The slash-command dispatcher in handlers/commands/index.js already
calls requireStaffRole before invoking any handler; the button
dispatcher needed the same gate. Now:
- confirm_delete_tag::<name> → requireStaffRole, then proceed.
- TICKET_BUTTON_HANDLERS dispatch → requireStaffRole, then proceed.
- FREE_BUTTON_HANDLERS (open_ticket* panel buttons, cancel_delete_tag)
remain ungated — those are public-facing by design.
requireStaffRole replies ephemerally ("This command is only available
to the support team (<@&role>)") and returns true when the caller
should bail, matching the slash-command behavior.
This commit is contained in:
@@ -34,6 +34,7 @@ const { addMemberToStaffThread, createStaffThread } = require('../services/staff
|
|||||||
const { pinMessage } = require('../services/pinMessage');
|
const { pinMessage } = require('../services/pinMessage');
|
||||||
const { logError, logTicketEvent } = require('../services/debugLog');
|
const { logError, logTicketEvent } = require('../services/debugLog');
|
||||||
const { findTicketForChannel, runDeferred } = require('./sharedHelpers');
|
const { findTicketForChannel, runDeferred } = require('./sharedHelpers');
|
||||||
|
const { requireStaffRole } = require('./commands/helpers');
|
||||||
|
|
||||||
const Ticket = mongoose.model('Ticket');
|
const Ticket = mongoose.model('Ticket');
|
||||||
const Transcript = mongoose.model('Transcript');
|
const Transcript = mongoose.model('Transcript');
|
||||||
@@ -732,16 +733,28 @@ async function handleButton(interaction) {
|
|||||||
const { customId } = interaction;
|
const { customId } = interaction;
|
||||||
|
|
||||||
// Tag-delete confirm has a dynamic id (`confirm_delete_tag::<name>`).
|
// Tag-delete confirm has a dynamic id (`confirm_delete_tag::<name>`).
|
||||||
|
// Mutates the Tag collection — staff only.
|
||||||
if (customId.startsWith('confirm_delete_tag::')) {
|
if (customId.startsWith('confirm_delete_tag::')) {
|
||||||
|
if (await requireStaffRole(interaction)) return;
|
||||||
return handleTagDeleteConfirm(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).
|
||||||
const freeHandler = FREE_BUTTON_HANDLERS[customId];
|
const freeHandler = FREE_BUTTON_HANDLERS[customId];
|
||||||
if (freeHandler) return freeHandler(interaction);
|
if (freeHandler) return freeHandler(interaction);
|
||||||
|
|
||||||
const ticketHandler = TICKET_BUTTON_HANDLERS[customId];
|
const ticketHandler = TICKET_BUTTON_HANDLERS[customId];
|
||||||
if (!ticketHandler) return;
|
if (!ticketHandler) return;
|
||||||
|
|
||||||
|
// Every TICKET_BUTTON_HANDLERS entry mutates ticket state
|
||||||
|
// (claim/close/confirm_close*/cancel_close/escalate*/deescalate). The slash
|
||||||
|
// command dispatcher in handlers/commands/index.js gates these via
|
||||||
|
// requireStaffRole; the button dispatcher must do the same — non-staff
|
||||||
|
// members with view access to the ticket channel (creator, /add'd users)
|
||||||
|
// could otherwise click Claim, Escalate, Close, etc.
|
||||||
|
if (await requireStaffRole(interaction)) return;
|
||||||
|
|
||||||
const ticket = await findTicketForChannel(
|
const ticket = await findTicketForChannel(
|
||||||
interaction,
|
interaction,
|
||||||
'This channel is not linked to a ticket, or the ticket could not be found.'
|
'This channel is not linked to a ticket, or the ticket could not be found.'
|
||||||
|
|||||||
Reference in New Issue
Block a user