Citizen allows stored XSS in Command Palette tip messages
Description
Citizen is a MediaWiki skin that makes extensions part of the cohesive experience. Multiple system messages are inserted into the CommandPaletteFooter 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.
Citizen MediaWiki skin 3.3.1 fixes stored XSS where system messages are rendered as raw HTML in the command palette, allowing users with editinterface to inject arbitrary HTML.
Vulnerability
Overview
CVE-2025-49575 is a stored cross-site scripting (XSS) vulnerability in the Citizen MediaWiki skin. The skin inserts multiple system messages into the CommandPaletteFooter component as raw HTML without proper sanitization. This allows any user who can edit those system messages (i.e., those with the editinterface user right) to inject arbitrary HTML into the DOM, leading to potential script execution [1].
Exploitation
Prerequisites
Exploitation requires a wiki where a user group has the editinterface right but not the editsitejs right. Attackers with this privilege can modify system messages such as citizen-command-palette-tip-commands or similar, which are then rendered unsanitized in the command palette footer. The vulnerability is triggered when a victim views a page that includes the command palette, causing the injected HTML to execute in their browser [1][3].
Impact
Successful exploitation allows an attacker to execute arbitrary JavaScript in the context of any user viewing the command palette. This can lead to session hijacking, defacement, or theft of sensitive data. The vulnerability is classified as stored XSS because the malicious payload persists in the system messages until removed [1].
Mitigation
The vulnerability is fixed in Citizen skin version 3.3.1. The fix, introduced in commit 93c36ac, ensures that system messages are properly escaped using Html::element() instead of raw HTML concatenation [3]. Users should upgrade to the latest version immediately. No workarounds are documented, but restricting the editinterface right to trusted users reduces the attack surface [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: >= 4fa69e1d062dca7e407cc0530cf1da3e2baaf0b5, < 93c36ac778397e0e7c46cf7adb1e5d848265f1bd
Patches
393c36ac77839fix(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>
4fa69e1d062dfeat(commandPalette): ✨ add tips for command palette usage
4 files changed · +43 −6
i18n/en.json+7 −1 modified@@ -108,5 +108,11 @@ "citizen-command-palette-queryaction-fulltext-search-description": "Search the full text of all pages", "citizen-command-palette-queryaction-media-search-description": "Search for media files", - "citizen-command-palette-queryaction-page-edit-description": "Create or edit page" + "citizen-command-palette-queryaction-page-edit-description": "Create or edit page", + + "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-actions": "Type <code>></code> for a list of actions" }
i18n/qqq.json+6 −1 modified@@ -99,5 +99,10 @@ "citizen-command-palette-command-user-description": "Description for the user command in the command palette", "citizen-command-palette-queryaction-fulltext-search-description": "Description for the fulltext search in the command palette", "citizen-command-palette-queryaction-media-search-description": "Description for the media search in the command palette", - "citizen-command-palette-queryaction-page-edit-description": "Description for the page edit action in the command palette" + "citizen-command-palette-queryaction-page-edit-description": "Description for the page edit action in the command palette", + "citizen-command-palette-tip-commands": "Tip for the commands in the command palette", + "citizen-command-palette-tip-users": "Tip for the users in the command palette", + "citizen-command-palette-tip-namespace": "Tip for the namespace in the command palette", + "citizen-command-palette-tip-templates": "Tip for the templates in the command palette", + "citizen-command-palette-tip-actions": "Tip for the actions in the command palette" }
resources/skins.citizen.commandPalette/components/CommandPaletteFooter.vue+25 −4 modified@@ -1,9 +1,7 @@ <template> <div class="citizen-command-palette__footer"> - <div class="citizen-command-palette__footer-note"> - Thanks for trying our new Command Palette! - <a href="https://github.com/StarCitizenTools/mediawiki-skins-Citizen/issues">Give us feedback</a> - </div> + <!-- eslint-disable-next-line vue/no-v-html --> + <div class="citizen-command-palette__footer-note" v-html="currentTip"></div> <command-palette-keyboard-hints :has-highlighted-item-with-actions="hasHighlightedItemWithActions" :item-count="itemCount" @@ -18,6 +16,7 @@ <script> const { defineComponent } = require( 'vue' ); +const { ref, computed, onMounted } = require( 'vue' ); const CommandPaletteKeyboardHints = require( './CommandPaletteKeyboardHints.vue' ); // @vue/component @@ -55,6 +54,28 @@ module.exports = exports = defineComponent( { type: Number, default: 0 } + }, + setup() { + // 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' ).text(), + mw.message( 'citizen-command-palette-tip-users' ).text(), + mw.message( 'citizen-command-palette-tip-namespace' ).text(), + mw.message( 'citizen-command-palette-tip-templates' ).text() + ]; + + const currentTipIndex = ref( 0 ); + const currentTip = computed( () => tips[ currentTipIndex.value ] ); + + onMounted( () => { + // Randomly select a tip when component is mounted + currentTipIndex.value = Math.floor( Math.random() * tips.length ); + } ); + + return { + currentTip + }; } } ); </script>
skin.json+5 −0 modified@@ -394,6 +394,11 @@ "citizen-command-palette-queryaction-fulltext-search-description", "citizen-command-palette-queryaction-media-search-description", "citizen-command-palette-queryaction-page-edit-description", + "citizen-command-palette-tip-commands", + "citizen-command-palette-tip-users", + "citizen-command-palette-tip-namespace", + "citizen-command-palette-tip-templates", + "citizen-command-palette-tip-actions", "citizen-command-palette-type-action", "citizen-command-palette-type-command", "citizen-command-palette-type-menu-item",
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
6- github.com/advisories/GHSA-4c2h-67qq-vm87ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-49575ghsaADVISORY
- github.com/StarCitizenTools/mediawiki-skins-Citizen/commit/4fa69e1d062dca7e407cc0530cf1da3e2baaf0b5ghsax_refsource_MISCWEB
- github.com/StarCitizenTools/mediawiki-skins-Citizen/commit/54c8717d45ce1594918f11cb9ce5d0ccd8dfee65ghsaWEB
- github.com/StarCitizenTools/mediawiki-skins-Citizen/commit/93c36ac778397e0e7c46cf7adb1e5d848265f1bdghsax_refsource_MISCWEB
- github.com/StarCitizenTools/mediawiki-skins-Citizen/security/advisories/GHSA-4c2h-67qq-vm87ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.