Area: mobieusHelp (audit p7) · Surface: mobieusHelp /api/help/ai/* (AgentAIController + HelpdeskAI) · Dimension: security · Severity: critical
OWASP A01 Broken Access Control / IDOR plus sensitive-data exposure. A role-2 member can POST /api/help/ai/summary {ticket_id:N} for any N and receive an AI-generated summary that incorporates internal notes (which the portal deliberately hides). They can also drive repeated calls to burn the tenant's BYOK Anthropic spend (AISpendTracker only records, does not authorize the actor against the ticket). The controller comment itself admits 'Finer agent-role gating + CSRF: tracked as a follow-up'.
Evidence
AgentAIController (src/Controllers/Helpdesk/AgentAIController.php) only checks `if (empty($_SESSION['user_id']))` (L39) — no agent role, no queue scope, no ticket ownership. summary() L54-58 -> HelpdeskAI::summary((int)$b['ticket_id']). HelpdeskAI::summary (src/Services/Helpdesk/HelpdeskAI.php L52-57) calls buildTicketContext($ticketId, includeFullThread:true). buildTicketContext (L204-238) does `ThreadEntry::listForTicket($ticketId, true)` — the `true` INCLUDES internal notes (L211) — for ANY ticket id, with no auth check. The AI output (summary / reply suggestion containing internal-note content) is returned to the caller. Routes registered in the ANONYMOUS group (routes.php L756-763; group opened L340 closed L771) — only the controller's session check stands.
Suggested fix. Require the caller to resolve to a helpdesk agent and to have a visible-queue role on the ticket's queue before any HelpdeskAI call; pass that resolution down so buildTicketContext only includes internal notes for users entitled to see them. Add per-ticket authorization in HelpdeskAI rather than trusting the controller.
Filed by the automated tenant-app audit and adversarially evidence-verified. Status: verified. Open — not yet actioned.
Patrick Bass
@mobieus