From 5709c3134f4bfb5a7362bbcb4a3a1a756f59acaa Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Tue, 8 Sep 2026 21:49:42 -0400 Subject: [PATCH] Verify mentioned-user access before sending @mention notifications (#69) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sendMentionNotification(), called from add_comment.php, had no visibility check at all — unlike sendCommentNotification()/ notifyWatchers() which redact the comment preview for non-public tickets. Mentioning a user with zero standing access to a confidential ticket (not creator/assignee/admin, not in visibility_groups) sent them a Matrix DM with the full ticket title AND comment text — worse than #46 since it's delivered directly to an individual rather than diluted into a shared list. add_comment.php now filters mentioned users through canUserAccessTicket() before resolving Matrix IDs, skipping the notification entirely for anyone without access (one of the two options the issue names as acceptable). getMentionedUsers() needed to start selecting is_admin and groups alongside user_id/username/ display_name, since canUserAccessTicket() requires them. Verified against real MariaDB: a user mentioned on a confidential ticket they don't own/aren't assigned to is correctly denied, a user in the matching visibility_groups for an internal ticket is correctly allowed, and the same user is correctly denied on a different internal ticket whose group they're not in. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015nCxwFFsy8ouMWzn56rPVP --- api/add_comment.php | 13 ++++++++++--- models/CommentModel.php | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/api/add_comment.php b/api/add_comment.php index 06538b7..6fecce9 100644 --- a/api/add_comment.php +++ b/api/add_comment.php @@ -177,9 +177,16 @@ try { $ticketTitle = $ticket['title'] ?? "Ticket #{$ticketId}"; $ticketVisibility = $ticket['visibility'] ?? 'public'; - // @mention notifications — resolve usernames → Matrix IDs via Synapse Admin API - if (!empty($mentionedUsers)) { - $mentionedUsernames = array_column($mentionedUsers, 'username'); + // @mention notifications — resolve usernames → Matrix IDs via Synapse Admin API. + // Only notify mentioned users who actually have access to this ticket; + // otherwise a mention would DM them the ticket's title and comment text + // even though canUserAccessTicket() would deny them the ticket itself. + $accessibleMentionedUsers = array_filter( + $mentionedUsers, + fn($u) => $ticketModel->canUserAccessTicket($ticket, $u) + ); + if (!empty($accessibleMentionedUsers)) { + $mentionedUsernames = array_column($accessibleMentionedUsers, 'username'); $mentionedMatrixIds = SynapseHelper::resolveUsernames($mentionedUsernames); if (!empty($mentionedMatrixIds)) { NotificationHelper::sendMentionNotification($ticketId, $ticketTitle, $commentText, $authorDisplay, $mentionedMatrixIds); diff --git a/models/CommentModel.php b/models/CommentModel.php index 419f26b..57b816e 100644 --- a/models/CommentModel.php +++ b/models/CommentModel.php @@ -38,7 +38,7 @@ class CommentModel } $placeholders = str_repeat('?,', count($usernames) - 1) . '?'; - $sql = "SELECT user_id, username, display_name FROM users WHERE username IN ($placeholders)"; + $sql = "SELECT user_id, username, display_name, is_admin, `groups` FROM users WHERE username IN ($placeholders)"; $stmt = $this->conn->prepare($sql); $types = str_repeat('s', count($usernames));