Citizen allows stored XSS in menu heading message
Description
Citizen is a MediaWiki skin that makes extensions part of the cohesive experience. All system messages in menu headings using the Menu.mustache template are inserted as raw HTML, allowing anybody who can edit those messages to insert arbitrary HTML into the DOM. This impacts wikis where a group has the editinterface but not the editsitejs user right. This vulnerability is fixed in 3.3.1.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
CVE-2025-49579 is a stored XSS in the Citizen MediaWiki skin via raw HTML insertion in system message menu headings.
Vulnerability
Description
CVE-2025-49579 is a stored cross-site scripting (XSS) vulnerability in the Citizen MediaWiki skin. The issue originates from the Menu.mustache template, which inserts all system messages used in menu headings as raw HTML without proper sanitization. This allows any user who can edit system messages (those with the editinterface right) to inject arbitrary HTML into the page DOM.[1]
Exploitation
An attacker with the editinterface user right—but without the more restricted editsitejs permission—can craft a malicious system message (e.g., citizen-command-palette-tip-commands) containing HTML or JavaScript. When the template renders this message in a menu heading, the injected payload executes in the context of any user viewing the affected page. The attack does not require direct interaction with the victim beyond normal page views.[1][2]
Impact
Successful exploitation enables arbitrary HTML injection and potential JavaScript execution. This can lead to session theft, defacement, or other client-side attacks against wiki users. The vulnerability is particularly impactful on wikis where trusted editors have editinterface privileges but lack editsitejs, as those editors are typically considered low-risk but can now achieve stored XSS.[1][2]
Mitigation
The vulnerability is fixed in Citizen skin version 3.3.1. The commit addresses the issue by escaping system message output in the command palette tips and other menu-related templates, using Html::element and `` tags to prevent raw HTML rendering.[2] Administrators are strongly advised to update to the latest version immediately. No workarounds are documented.[1]
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
starcitizentools/citizen-skinPackagist | >= 2.4.2, < 3.3.1 | 3.3.1 |
Affected products
2- StarCitizenTools/mediawiki-skins-Citizenv5Range: >= 54c8717d45ce1594918f11cb9ce5d0ccd8dfee65, < 93c36ac778397e0e7c46cf7adb1e5d848265f1bd
Patches
293c36ac77839fix(security): 🐛 🔒️ fix various stored XSS system message vulnerabilities
7 files changed · +23 −17
i18n/en.json+1 −1 modified@@ -116,6 +116,6 @@ "citizen-command-palette-tip-commands": "Type <code>/</code> for all commands", "citizen-command-palette-tip-users": "Type <code>@</code> to search for users", "citizen-command-palette-tip-namespace": "Type <code>:</code> to search for namespace", - "citizen-command-palette-tip-templates": "Type <code>{{</code> to search for templates", + "citizen-command-palette-tip-templates": "Type <code><nowiki>{{</nowiki></code> to search for templates", "citizen-command-palette-tip-actions": "Type <code>></code> for a list of actions" }
includes/Components/CitizenComponentUserInfo.php+10 −5 modified@@ -4,6 +4,7 @@ namespace MediaWiki\Skins\Citizen\Components; +use MediaWiki\Html\Html; use MediaWiki\Language\Language; use MediaWiki\MediaWikiServices; use MediaWiki\Title\MalformedTitleException; @@ -52,11 +53,15 @@ private function getUserRegistration(): ?array { return null; } - $html = sprintf( - '<time class="citizen-user-regdate" datetime="%s">%s</time>', - wfTimestamp( TS_ISO_8601, $timestamp ), - // Since this is not accessible by anon, we can use user language - $this->lang->userDate( $timestamp, $this->user ) + // Since this is not accessible by anon, we can use user language + $date = $this->lang->userDate( $timestamp, $this->user ); + $html = Html::element( + 'time', + [ + 'class' => 'citizen-user-regdate', + 'datetime' => wfTimestamp( TS_ISO_8601, $timestamp ) + ], + $date ); return [
resources/skins.citizen.commandPalette/components/CommandPaletteFooter.vue+4 −4 modified@@ -59,10 +59,10 @@ module.exports = exports = defineComponent( { // TODO: Make this expandable with more tips, probably with a mw hook // TODO: Maybe we should move this to store? const tips = [ - mw.message( 'citizen-command-palette-tip-commands' ).plain(), - mw.message( 'citizen-command-palette-tip-users' ).plain(), - mw.message( 'citizen-command-palette-tip-namespace' ).plain(), - mw.message( 'citizen-command-palette-tip-templates' ).plain() + mw.message( 'citizen-command-palette-tip-commands' ).parse(), + mw.message( 'citizen-command-palette-tip-users' ).parse(), + mw.message( 'citizen-command-palette-tip-namespace' ).parse(), + mw.message( 'citizen-command-palette-tip-templates' ).parse() ]; const currentTipIndex = ref( 0 );
resources/skins.citizen.preferences/addPortlet.polyfill.js+1 −1 modified@@ -15,7 +15,7 @@ function addDefaultPortlet( portlet ) { if ( label ) { const labelDiv = document.createElement( 'div' ); labelDiv.classList.add( 'citizen-menu__heading' ); - labelDiv.innerHTML = label.textContent || ''; + labelDiv.textContent = label.textContent || ''; portlet.insertBefore( labelDiv, label ); label.remove(); }
resources/skins.citizen.search/templates/TypeaheadPlaceholder.mustache+4 −4 modified@@ -1,11 +1,11 @@ -<div +<div class="citizen-typeahead-placeholder" id="citizen-typeahead-placeholder" {{#hidden}}hidden{{/hidden}} > <div class="citizen-typeahead-placeholder-content"> {{#icon}}<div class="citizen-typeahead-placeholder-icon citizen-ui-icon mw-ui-icon-wikimedia-{{.}}"></div>{{/icon}} - {{#title}}<div class="citizen-typeahead-placeholder-title">{{{.}}}</div>{{/title}} - {{#description}}<div class="citizen-typeahead-placeholder-description">{{{.}}}</div>{{/description}} + {{#title}}<div class="citizen-typeahead-placeholder-title">{{.}}</div>{{/title}} + {{#description}}<div class="citizen-typeahead-placeholder-description">{{.}}</div>{{/description}} </div> -</div> \ No newline at end of file +</div>
skin.json+1 −0 modified@@ -312,6 +312,7 @@ "class": "MediaWiki\\ResourceLoader\\CodexModule", "dependencies": [ "mediawiki.api", + "mediawiki.jqueryMsg", "mediawiki.language", "mediawiki.page.ready", "mediawiki.storage",
templates/Menu.mustache+2 −2 modified@@ -6,8 +6,8 @@ > {{#label}} <div class="citizen-menu__heading{{#label-class}} {{.}}{{/label-class}}"> - {{{.}}} + {{.}} </div> {{/label}} {{>MenuContents}} -</nav> \ No newline at end of file +</nav>
54c8717d45cerefactor(core): ♻️ simplify menu header implementation
7 files changed · +29 −37
includes/Partials/Drawer.php+0 −6 modified@@ -47,9 +47,6 @@ final class Drawer extends Partial { * @throws Exception */ public function decorateSidebarData( $sidebarData ) { - // Enable label for first portlet - $sidebarData['data-portlets-first']['has-label'] = true; - $globalToolsId = $this->getConfigValue( 'CitizenGlobalToolsPortlet' ); $globalToolsHtml = $this->getGlobalToolsHTML(); $globalToolsAdded = false; @@ -62,9 +59,6 @@ public function decorateSidebarData( $sidebarData ) { } for ( $i = 0; $i < count( $sidebarData['array-portlets-rest'] ); $i++ ) { - // Enable label for other portlet - $sidebarData['array-portlets-rest'][$i]['has-label'] = true; - switch ( $sidebarData['array-portlets-rest'][$i]['id'] ) { // Remove toolbox since it is handled by page tools case 'p-tb': {
includes/Partials/PageTools.php+0 −1 modified@@ -93,7 +93,6 @@ private function getArticleToolsData( $sidebarData ) { foreach ( $sidebarData['array-portlets-rest'] as $portlet ) { if ( $portlet['id'] === 'p-tb' ) { $data = $portlet; - $data['has-label'] = true; $data['is-empty'] = false; break; }
includes/SkinCitizen.php+0 −6 modified@@ -108,12 +108,6 @@ public function getTemplateData(): array { $data += $tools->getPageToolsData( $parentData ); - // Show some portlet labels - // NOTE: This is only placed here temporarily - if ( $parentData['data-portlets']['data-variants']['is-empty'] === false ) { - $parentData['data-portlets']['data-variants']['has-label'] = true; - } - return array_merge( $parentData, $data ); }
resources/skins.citizen.styles/components/Header.less+5 −0 modified@@ -89,6 +89,11 @@ background-color: var( --background-color-primary--active ); } } + + // Hide header menu labels + .citizen-menu__heading { + .mixin-screen-reader-text; + } } &__start,
resources/skins.citizen.styles/components/Menu.less+7 −9 modified@@ -1,14 +1,12 @@ .citizen-menu { &__heading { - &-label { - display: block; - padding: 0.625rem var( --space-md ); - margin: 0; - color: var( --color-base--subtle ); - font-size: inherit; - font-weight: var( --font-weight-normal ); - letter-spacing: 0.05em; - } + display: block; + padding: 0.625rem var( --space-md ); + margin: 0; + color: var( --color-base--subtle ); + font-size: inherit; + font-weight: var( --font-weight-normal ); + letter-spacing: 0.05em; } }
resources/skins.citizen.styles/components/Pagetools.less+5 −0 modified@@ -112,6 +112,11 @@ .citizen-menu-checkbox-checkbox:checked ~ .page-actions__button { background-color: var( --background-color-primary--active ); } + + // Hide menu labels + > .citizen-menu > .citizen-menu__heading { + .mixin-screen-reader-text; + } } // Language counter badge
templates/Menu.mustache+12 −15 modified@@ -1,16 +1,13 @@ -{{! - -}} -<nav id="{{id}}"{{#class}} class="{{.}}"{{/class}} {{{html-tooltip}}} {{{html-userlangattributes}}}> - {{#has-label}} - <label - id="{{id}}-label" - {{#aria-label}} aria-label="{{.}}"{{/aria-label}} - class="citizen-menu__heading{{#heading-class}} {{.}}{{/heading-class}}"> - <span class="citizen-menu__heading-label">{{label}}</span> - </label> - {{/has-label}} - {{{html-before-portal}}} - <ul>{{{html-items}}}</ul> - {{{html-after-portal}}} +{{! + +}} +<nav id="{{id}}" class="citizen-menu{{#class}} {{.}}{{/class}}" {{{html-tooltip}}} {{{html-userlangattributes}}}> + {{#label}} + <div class="citizen-menu__heading{{#label-class}} {{.}}{{/label-class}}"> + {{{.}}} + </div> + {{/label}} + {{{html-before-portal}}} + <ul>{{{html-items}}}</ul> + {{{html-after-portal}}} </nav> \ No newline at end of file
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-g3cp-pq72-hjpvghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-49579ghsaADVISORY
- github.com/StarCitizenTools/mediawiki-skins-Citizen/commit/54c8717d45ce1594918f11cb9ce5d0ccd8dfee65ghsax_refsource_MISCWEB
- github.com/StarCitizenTools/mediawiki-skins-Citizen/commit/93c36ac778397e0e7c46cf7adb1e5d848265f1bdghsax_refsource_MISCWEB
- github.com/StarCitizenTools/mediawiki-skins-Citizen/security/advisories/GHSA-g3cp-pq72-hjpvghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.