CVE-2026-54049: Sakai Conversations Stored XSS via Unsanitized Topic and Post Messages
Any authenticated Sakai user can inject arbitrary HTML and JavaScript into Conversations topics, posts, or comments, which then silently executes in every other course member's browser when they open…

The problem
The Conversations REST API accepts a message field for topics (POST /api/sites/{siteId}/topics), post replies, and comments. The service layer stores that field directly to the database without calling FormattedText.processFormattedText() or any other HTML sanitizer.
On the frontend, SakaiTopic.js, SakaiPost.js, and SakaiComment.js all render the stored message with LitElement's unsafeHTML() directive. Because the content is never cleaned at write time or read time, injected script tags and event handlers execute in every viewer's browser session.
Proof of concept
A working proof-of-concept for CVE-2026-54049 in org.sakaiproject.conversations:sakai-conversations-impl, with the exact payload below.
POST /api/sites/BELP_275K_7418/topics HTTP/1.1
Host: sakai.example.edu
Cookie: SAKAIID=<authenticated-session>
Content-Type: application/json
{"title":"XSS Test Topic","message":"<img src=x onerror=alert(document.cookie)>","type":"QUESTION","visibility":"SITE","draft":false}The root cause is a missing sanitization call in ConversationsServiceImpl. Other Sakai tools (Announcements, Assignments, Resources) pass user HTML through FormattedText.processFormattedText() before persistence; the Conversations implementation never did.
The patch (commit 2696b4b48cbef2e81512f52f84f7477adff78b27) adds that call for topic, post, and comment message fields before the database write, so malicious tags are stripped or escaped at storage time rather than relying on the frontend to be safe.
Because unsafeHTML() is intentional in LitElement for rich-text rendering, the only correct fix is sanitizing on ingress. CWE-79 applies cleanly: unsanitized user input reaches a rendering context that treats it as markup.
The fix
Upgrade to Sakai 23.5 or later. The fix is in commit 2696b4b48cbef2e81512f52f84f7477adff78b27, which adds FormattedText.processFormattedText() calls in ConversationsServiceImpl for all three content types (topics, posts, comments) before any database write.
No configuration workaround exists for older versions.
Related research
- high · 8.7CVE-2026-55839CVE-2026-55839: Kestra Stored XSS via Custom Markdown [[link]] Attribute Injection
- high · 7.5OmniFaces: Forged Resource IDs, XSS via o:hashParam, and Push-Channel Replay
- critical · 9.8CVE-2026-76904CVE-2026-76904: GeoTools gt-jdbc-postgis Unauthenticated SQL Injection via jsonArrayContains
- highCVE-2026-54251CVE-2026-54251: netty-incubator-codec-ohttp Native Direct-Memory Leak on AEAD Decryption Failure