Forums Bug Reports Thread

Portal ticket thread renders body_html unescaped — stored-XSS sink with no sanitizer at the boundary

Patrick Bass · Jun 6 · 18 · 1 Locked
[Minor] [Normal Priority] [Bug Fixed] [Always Reproduces]
🚀 OP Jun 6, 2026 6:25pm

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

🚀 Jun 7, 2026 5:38am

Resolved — fixed and deployed. Commit 89b2334003a7, shipped dev-first then to all tenants on 2026-06-06.

Replaced raw `<?= $entry['body_html'] ?>` echo on line 43 with `\App\Services\MarkdownService::purify((string) $entry['body_html'])`, purifying thread-entry HTML at the render boundary via the existing HTMLPurifier helper (same pattern as forums/post-history.php). Kept the escaped body_text fallback for empty body_html. php -l passes.

Status: fixed. Thread closed and locked.


Patrick Bass
@mobieus

Log in or register to reply to this thread.