Miggo Logo

CVE-2025-49575:
Citizen skin vulnerable to stored XSS through multiple system messages

6.5

CVSS Score
3.1

Basic Information

EPSS Score
0.09923%
Published
6/11/2025
Updated
6/13/2025
KEV Status
No
Technology
TechnologyPHP

Technical Details

CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:N
Package NameEcosystemVulnerable VersionsFirst Patched Version
starcitizentools/citizen-skincomposer>= 2.4.2, < 3.3.13.3.1

Vulnerability Intelligence
Miggo AIMiggo AI

Miggo AIRoot Cause Analysis

The vulnerability (GHSA-4c2h-67qq-vm87 / CVE-2025-49575) encompasses multiple stored Cross-Site Scripting (XSS) flaws within the StarCitizenTools/mediawiki-skins-Citizen skin. The fundamental cause across these issues is the improper handling and sanitization of data, particularly system messages or other content that can be influenced by users with editinterface permissions, before it is rendered as HTML in the user's browser.

The primary vulnerability, as highlighted in the advisory, resides in the CommandPaletteFooter.vue component. System messages intended as tips were fetched using mw.message(...).plain(). This method does not escape HTML entities within the message content. Subsequently, these potentially unsafe messages were rendered into the DOM using Vue's v-html directive, which explicitly allows raw HTML rendering, thereby creating an XSS vector.

The security patch (commit 93c36ac778397e0e7c46cf7adb1e5d848265f1bd) addressed this specific issue by changing .plain() to .parse(), which is designed to handle wikitext and produce safe HTML output suitable for rendering.

Beyond this main issue, the same commit also rectified several other XSS vulnerabilities:

  1. In includes/Components/CitizenComponentUserInfo.php, the getUserRegistration method was modified to use Html::element for constructing HTML instead of sprintf with potentially unescaped date strings.
  2. In resources/skins.citizen.preferences/addPortlet.polyfill.js, an assignment to innerHTML was replaced with an assignment to textContent to prevent potential HTML injection through portlet labels.
  3. In Mustache templates (templates/Menu.mustache and resources/skins.citizen.search/templates/TypeaheadPlaceholder.mustache), triple-stashes ({{{.}}}), which render unescaped HTML, were replaced with double-stashes ({{.}}) to ensure data is properly escaped before rendering.

These fixes collectively mitigate the risk of attackers injecting malicious scripts by controlling the content of various system messages or other data points within the wiki interface.

Vulnerable functions

CommandPaletteFooter.setup
resources/skins.citizen.commandPalette/components/CommandPaletteFooter.vue
The `setup` function of the `CommandPaletteFooter.vue` component was vulnerable because it retrieved system messages (tips) using `mw.message(...).plain()`. The `.plain()` method returns the message as plain text, but does not escape any HTML entities if the message itself contains HTML. These messages were then assigned to the `currentTip` computed property, which was rendered into the DOM using the `v-html` directive. This allowed for stored XSS if an attacker could control the content of these system messages (e.g., `citizen-command-palette-tip-commands`). The fix was to use `mw.message(...).parse()` which processes messages as wikitext and outputs safe HTML.
MediaWiki\Skins\Citizen\Components\CitizenComponentUserInfo::getUserRegistration
includes/Components/CitizenComponentUserInfo.php
The `getUserRegistration` method in `CitizenComponentUserInfo.php` previously used `sprintf` to construct HTML for displaying a user's registration date. The date string, obtained from `$this->lang->userDate()`, was directly embedded into the HTML. If the output of `userDate` could be manipulated to contain malicious HTML (though less likely for date functions, it's a potential risk if underlying localization messages are compromised), it would lead to XSS. The fix replaced `sprintf` with `Html::element`, which is a safer way to construct HTML and typically handles escaping of content.
addDefaultPortlet
resources/skins.citizen.preferences/addPortlet.polyfill.js
The `addDefaultPortlet` function in `addPortlet.polyfill.js` used `labelDiv.innerHTML = label.textContent`. While `label.textContent` is supposed to return only the text content (stripping HTML), assigning it via `innerHTML` can be risky if there are edge cases or misunderstandings about the source or nature of `label.textContent`. If `label.textContent` could somehow contain characters that `innerHTML` interprets in a way that leads to script execution (e.g., if it wasn't properly sanitized upstream or if `textContent` behaved unexpectedly in some browser/context), it could lead to XSS. The fix changes the assignment to `labelDiv.textContent`, which is inherently safer as it treats the assigned string purely as text.
Mustache template rendering for Menu.mustache
templates/Menu.mustache
The `Menu.mustache` template used triple-stashes (`{{{.}}}`) to render a label. Triple-stashes in Mustache render HTML content without escaping. If the data provided for the label (represented by `.`) contained malicious HTML, it would be injected directly into the page, leading to XSS. The fix changed `{{{.}}}` to `{{.}}` (double-stashes), which ensures the data is HTML-escaped before rendering.
Mustache template rendering for TypeaheadPlaceholder.mustache
resources/skins.citizen.search/templates/TypeaheadPlaceholder.mustache
The `TypeaheadPlaceholder.mustache` template used triple-stashes (`{{{.}}}`) to render title and description. Triple-stashes in Mustache render HTML content without escaping. If the data provided for the title or description contained malicious HTML, it would be injected directly into the page, leading to XSS. The fix changed `{{{.}}}` to `{{.}}` (double-stashes), ensuring the data is HTML-escaped.

WAF Protection Rules

WAF Rule

### Summ*ry Multipl* syst*m m*ss***s *r* ins*rt** into t** *omm*n*P*l*tt**oot*r *s r*w *TML, *llowin* *ny*o*y w*o **n **it t*os* m*ss***s to ins*rt *r*itr*ry *TML into t** *OM. ### **t*ils T** m*ss***s *r* r*tri*v** usin* t** `pl*in()` output mo**:

Reasoning

T** vuln*r**ility (**S*-****-**qq-vm** / *V*-****-*****) *n*omp*ss*s multipl* stor** *ross-Sit* S*riptin* (XSS) *l*ws wit*in t** St*r*itiz*nTools/m**i*wiki-skins-*itiz*n skin. T** *un**m*nt*l **us* **ross t**s* issu*s is t** improp*r **n*lin* *n* s*n