VYPR
Moderate severityNVD Advisory· Published Jun 12, 2025· Updated Jun 12, 2025

Citizen allows stored XSS in menu heading message

CVE-2025-49579

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.

PackageAffected versionsPatched versions
starcitizentools/citizen-skinPackagist
>= 2.4.2, < 3.3.13.3.1

Affected products

2
  • Citizen/Citizenllm-fuzzy
    Range: <3.3.1
  • StarCitizenTools/mediawiki-skins-Citizenv5
    Range: >= 54c8717d45ce1594918f11cb9ce5d0ccd8dfee65, < 93c36ac778397e0e7c46cf7adb1e5d848265f1bd

Patches

2
93c36ac77839

fix(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>
    
    
54c8717d45ce

refactor(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

News mentions

0

No linked articles in our index yet.