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

Citizen allows stored XSS in user registration date message

CVE-2025-49578

Description

Citizen is a MediaWiki skin that makes extensions part of the cohesive experience. Various date messages returned by Language::userDate are inserted into 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.

Stored XSS in Citizen MediaWiki skin via unsanitized date messages allows users with editinterface rights to inject arbitrary HTML.

Vulnerability

Overview

The Citizen MediaWiki skin, which integrates extensions into a cohesive interface, contains a stored cross-site scripting (XSS) vulnerability in how it handles date messages. The Language::userDate method returns raw, unescaped strings that are then inserted into HTML using sprintf() without sanitization. Specifically, in the getUserRegistration() component, the date returned by $this->lang->userDate() is placed directly inside a `` element, allowing arbitrary HTML to be injected [1][2][4].

Exploitation

An attacker who can edit system messages (i.e., has the editinterface user right) but does not have editsitejs can exploit this flaw. By modifying the date-related interface messages (such as month names that appear in user registration dates), they can inject malicious HTML into the DOM. The attack vector requires the victim to view a page that displays the crafted message, and the payload is rendered by the browser, leading to stored XSS [1][4].

Impact

Successful exploitation allows an attacker to execute arbitrary HTML and potentially JavaScript in the context of the wiki, leading to session hijacking, defacement, or further privilege escalation. This is particularly dangerous on wikis where trusted editors have the editinterface right but lack the ability to edit site-wide JavaScript [1][4].

Mitigation

The vulnerability is fixed in Citizen version 3.3.1. The patch, introduced in commit 93c36ac, replaces raw string interpolation with Html::element(), which properly escapes the output. Users are advised to update to the latest version immediately [2][4].

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
>= 3.3.0, < 3.3.13.3.1

Affected products

2
  • Citizen/Citizenllm-fuzzy
    Range: <3.3.1
  • StarCitizenTools/mediawiki-skins-Citizenv5
    Range: >= 64cb5d7ab3a6dc0381fae54b31e8fc4afadc8beb, < 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>
    
    
64cb5d7ab3a6

feat(contentEnhancements): ✨ add user anniversary feature and improve registration date display

7 files changed · +57 10
  • includes/Components/CitizenComponentPageHeading.php+8 2 modified
    @@ -86,8 +86,14 @@ private function buildUserTagline(): string {
     		}
     
     		if ( is_string( $regDate ) ) {
    -			$regDateTs = wfTimestamp( TS_UNIX, $regDate );
    -			$msgRegDate = $localizer->msg( 'citizen-tagline-user-regdate', $this->pageLang->userDate( new MWTimestamp( $regDate ), $user ) );
    +			$regDateTs = wfTimestamp( TS_ISO_8601, $regDate );
    +			$regDateHtml = sprintf(
    +				'<time class="citizen-user-regdate" datetime="%s">%s</time>',
    +				$regDateTs,
    +				$this->pageLang->userDate( new MWTimestamp( $regDate ), $user )
    +			);
    +
    +			$msgRegDate = $localizer->msg( 'citizen-tagline-user-regdate', $regDateHtml );
     			$tagline .= "<span id=\"citizen-tagline-user-regdate\" data-user-regdate=\"$regDateTs\">$msgRegDate</span>";
     		}
     
    
  • includes/Components/CitizenComponentUserInfo.php+2 5 modified
    @@ -52,14 +52,11 @@ private function getUserRegistration(): ?array {
     			return null;
     		}
     
    -		$isAnniversary = substr( $timestamp, 4, 4 ) === substr( wfTimestampNow(), 4, 4 );
    -
     		$html = sprintf(
    -			'<time datetime="%s">%s</time>%s',
    +			'<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 ),
    -			$isAnniversary ? ' <span aria-label="anniversary">🎂</span>' : ''
    +			$this->lang->userDate( $timestamp, $this->user )
     		);
     
     		return [
    
  • resources/.eslintrc.json+2 1 modified
    @@ -22,7 +22,8 @@
     		"es-x/no-rest-spread-properties": "warn",
     		"es-x/no-symbol-prototype-description": "warn",
     		"compat/compat": "warn",
    -		"mediawiki/class-doc": "off"
    +		"mediawiki/class-doc": "off",
    +		"mediawiki/no-nodelist-unsupported-methods": "off"
     	},
     	"parserOptions": {
     		"ecmaVersion": 11,
    
  • resources/skins.citizen.scripts/contentEnhancements.js+40 0 added
    @@ -0,0 +1,40 @@
    +/**
    + * Various enhancements to the page content
    + */
    +
    +/**
    + * @return {void}
    + */
    +function init() {
    +	addUserAnniversary();
    +}
    +
    +/**
    + * Append cake emoji to user registration date if it's the user's anniversary
    + *
    + * @return {void}
    + */
    +function addUserAnniversary() {
    +	document.querySelectorAll( '.citizen-user-regdate' ).forEach( ( date ) => {
    +		const timestamp = date.getAttribute( 'datetime' );
    +		const anniversary = new Date( timestamp );
    +		const today = new Date();
    +
    +		if (
    +			anniversary.getMonth() !== today.getMonth() ||
    +			anniversary.getDate() !== today.getDate()
    +		) {
    +			return;
    +		}
    +
    +		const cake = document.createElement( 'span' );
    +		cake.textContent = ' 🎂';
    +		cake.classList.add( 'citizen-user-regdate-anniversary' );
    +		cake.setAttribute( 'aria-label', 'anniversary' );
    +		date.insertAdjacentElement( 'beforeend', cake );
    +	} );
    +}
    +
    +module.exports = {
    +	init: init
    +};
    
  • resources/skins.citizen.scripts/search.js+0 1 modified
    @@ -196,7 +196,6 @@ function renderSearchClearButton( input ) {
      * @return {void}
      */
     function bindSearchTrigger( details ) {
    -	// eslint-disable-next-line mediawiki/no-nodelist-unsupported-methods
     	document.querySelectorAll( '.citizen-search-trigger' ).forEach( ( trigger ) => {
     		trigger.addEventListener( 'click', () => openSearch( details ) );
     	} );
    
  • resources/skins.citizen.scripts/skin.js+4 1 modified
    @@ -55,12 +55,15 @@ function registerServiceWorker() {
     function initBodyContent( bodyContent ) {
     	const
     		sections = require( './sections.js' ),
    -		overflowElements = require( './overflowElements.js' );
    +		overflowElements = require( './overflowElements.js' ),
    +		contentEnhancements = require( './contentEnhancements.js' );
     
     	// Collapsable sections
     	sections.init( bodyContent );
     	// Overflow element enhancements
     	overflowElements.init( bodyContent );
    +	// Content enhancements
    +	contentEnhancements.init();
     }
     
     /**
    
  • skin.json+1 0 modified
    @@ -151,6 +151,7 @@
     					"name": "resources/skins.citizen.scripts/config.json",
     					"callback": "MediaWiki\\Skins\\Citizen\\Hooks\\ResourceLoaderHooks::getCitizenResourceLoaderConfig"
     				},
    +				"resources/skins.citizen.scripts/contentEnhancements.js",
     				"resources/skins.citizen.scripts/deferUntilFrame.js",
     				"resources/skins.citizen.scripts/dropdown.js",
     				"resources/skins.citizen.scripts/echo.js",
    

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.