Area: mobieusHelp (audit p7) · Surface: mobieusHelp portal ticket view (templates/helpdesk/portal/ticket.php) · Dimension: security · Severity: minor
OWASP A03 Injection (XSS) — defense-in-depth failure. The render boundary trusts every upstream writer to pre-escape. CLAUDE.md mandates ezyang/htmlpurifier is a dependency and '$e() for ALL output'; this sink bypasses both. The safety is incidental, not enforced, so a one-line change elsewhere reopens stored XSS against requesters.
Evidence
templates/helpdesk/portal/ticket.php L43: `<div class="p-entry__body"><?= $entry['body_html'] ?: $e((string) ($entry['body_text'] ?? '')) ?></div>` — body_html is echoed RAW (no $e(), no HtmlPurifier). Today the writers happen to be safe (PortalController::reply L300 and AgentController::submitReply L224 both do nl2br(htmlspecialchars(...)); InboundEmailProcessor.php L134 sets body_html=>null and strip_tags), but ThreadEntry::append (src/Models/Helpdesk/ThreadEntry.php L47) stores body_html verbatim with no sanitization, so any future writer that stores raw HTML (e.g. enabling provider HTML email bodies, an importer, or the v1 API if extended) yields stored XSS executing in the requester's browser.
Suggested fix. Run body_html through HtmlPurifier at the render boundary (or when persisting in ThreadEntry::append) instead of echoing raw. Given inbound HTML is currently stripped anyway, the lowest-risk fix is to purify in the template/partial that displays thread entries on both portal and agent surfaces.
Filed by the automated tenant-app audit and adversarially evidence-verified. Status: verified. Open — not yet actioned.
Patrick Bass
@mobieus