From 2ef50ee43db7d60a8b3df6ad7b4641631f521f0a Mon Sep 17 00:00:00 2001
From: SomeRandomDeveloper <thisisnotmyname275@gmail.com>
Date: Tue, 4 Nov 2025 18:39:38 +0100
Subject: [PATCH] SECURITY: Fix several stored i18n XSS vulnerabilities

* Escape error messages passed to mw.errorDialog and
  setStatus by default, except if they're objects.
* Use parseDom instead of .text() where necessary to
  make sure custom formatting in the affected messages
  doesn't break.
* Escape 'mwe-upwiz-other-v2' message when passing it to
  .append()

Bug: T407157
Change-Id: I16de2211594ea9a686868ad7789f9879bf981fa1
---
 resources/details/uw.TitleDetailsWidget.js | 12 +++++++++++-
 resources/mw.FlickrChecker.js              | 14 +++++++-------
 resources/mw.UploadWizardDetails.js        | 12 +++++++++---
 resources/mw.errorDialog.js                |  2 +-
 resources/ui/steps/uw.ui.Upload.js         |  2 +-
 5 files changed, 29 insertions(+), 13 deletions(-)

diff --git a/resources/details/uw.TitleDetailsWidget.js b/resources/details/uw.TitleDetailsWidget.js
index d01d05ef..d580fe13 100644
--- a/resources/details/uw.TitleDetailsWidget.js
+++ b/resources/details/uw.TitleDetailsWidget.js
@@ -207,6 +207,10 @@
 			if ( !mw.message( messageKey ).exists() ) {
 				messageKey = 'mwe-upwiz-blacklisted-details';
 			}
+			if ( messageKey === 'mwe-upwiz-blacklisted-details-titleblacklist-custom-filename' ) {
+				// temporarily hardcode message due to T407157
+				mw.messages.set( 'mwe-upwiz-blacklisted-details-titleblacklist-custom-filename-text', `<p><b>Examples of good file names:</b></p><ul><li>Nodnol skyline from Nodnol City Hall - Aug 2022</li><li>1875 Meeting of Settlers at Falconer Bay, New Nodland</li><li>Pseudohedron with no vertex visible from center</li></ul><br><p><b>Examples of bad file names:</b></p><ul><li>Image01</li><li>Joe</li><li>DSC00001</li><li>Foo.svg.png</li><li>30996951316264l</li><li>PSEUDOHEDRON WITH NO VERTEX VISIBLE FROM CENTER</li></ul><p>[https://commons.wikimedia.org/wiki/MediaWiki:Titleblacklist-custom-filename Learn more]</p>` );
+			}
 
 			const messageParams = [
 				messageKey,
@@ -215,7 +219,13 @@
 					const titleMessage = mw.message( messageKey + '-title' ),
 						title = titleMessage.exists() ? titleMessage.text() : '',
 						textMessage = mw.message( messageKey + '-text' ),
-						text = textMessage.exists() ? textMessage.text() : result.blacklist.blacklistReason;
+						text = textMessage.exists() ? textMessage.parseDom() : result.blacklist.blacklistReason;
+
+					if ( typeof text === 'object' ) {
+						// T407157: Links created by jqueryMsg don't open in a new tab, but we don't want the user to
+						// lose their progress when clicking on a link. Therefore, we manually fix this here.
+						text.find( 'a' ).attr( 'target', '_blank' );
+					}
 
 					mw.errorDialog( text, title );
 				}
diff --git a/resources/mw.FlickrChecker.js b/resources/mw.FlickrChecker.js
index a0aaf99b..49d62bcd 100644
--- a/resources/mw.FlickrChecker.js
+++ b/resources/mw.FlickrChecker.js
@@ -124,7 +124,7 @@ mw.FlickrChecker.prototype = {
 			}
 		} else {
 			// XXX show user the message that the URL entered was not valid
-			mw.errorDialog( mw.message( 'mwe-upwiz-url-invalid', 'Flickr' ).escaped() );
+			mw.errorDialog( mw.msg( 'mwe-upwiz-url-invalid', 'Flickr' ) );
 			this.$spinner.remove();
 			this.ui.flickrInterfaceReset();
 		}
@@ -366,7 +366,7 @@ mw.FlickrChecker.prototype = {
 				photoset = data.photos;
 			}
 			if ( !photoset ) {
-				$.Deferred().reject( mw.message( 'mwe-upwiz-url-invalid', 'Flickr' ).escaped() );
+				$.Deferred().reject( mw.msg( 'mwe-upwiz-url-invalid', 'Flickr' ) );
 			}
 			return photoset;
 		} );
@@ -484,7 +484,7 @@ mw.FlickrChecker.prototype = {
 			} );
 
 			if ( this.imageUploads.length === 0 ) {
-				return $.Deferred().reject( mw.message( 'mwe-upwiz-license-photoset-invalid' ).escaped() );
+				return $.Deferred().reject( mw.msg( 'mwe-upwiz-license-photoset-invalid' ) );
 			} else {
 				// eslint-disable-next-line no-jquery/no-global-selector
 				$( '#mwe-upwiz-flickr-select-list-container' ).show();
@@ -521,14 +521,14 @@ mw.FlickrChecker.prototype = {
 			photo_id: photoId
 		} ).then( ( data ) => {
 			if ( !data.photo ) {
-				return $.Deferred().reject( mw.message( 'mwe-upwiz-url-invalid', 'Flickr' ).escaped() );
+				return $.Deferred().reject( mw.msg( 'mwe-upwiz-url-invalid', 'Flickr' ) );
 			}
 			return data.photo;
 		} ).then( ( photo ) => {
 			const isBlacklistedPromise = this.isBlacklisted( photo.owner.nsid, photo.owner.path_alias );
 			return isBlacklistedPromise.then( ( isBlacklisted ) => {
 				if ( isBlacklisted ) {
-					return $.Deferred().reject( mw.message( 'mwe-upwiz-user-blacklisted', 'Flickr' ).escaped() );
+					return $.Deferred().reject( mw.msg( 'mwe-upwiz-user-blacklisted', 'Flickr' ) );
 				} else {
 					return photo;
 				}
@@ -734,7 +734,7 @@ mw.FlickrChecker.prototype = {
 				}
 				upload.url = largestSize.source;
 			} else {
-				mw.errorDialog( mw.message( 'mwe-upwiz-error-no-image-retrieved', 'Flickr' ).escaped() );
+				mw.errorDialog( mw.msg( 'mwe-upwiz-error-no-image-retrieved', 'Flickr' ) );
 				this.$spinner.remove();
 				this.ui.flickrInterfaceReset();
 				return $.Deferred().reject();
@@ -751,7 +751,7 @@ mw.FlickrChecker.prototype = {
 		// Set the license message to show the user.
 		let licenseMessage;
 		if ( licenseValue === 'invalid' ) {
-			licenseMessage = mw.msg( 'mwe-upwiz-license-external-invalid', 'Flickr', licenseName );
+			licenseMessage = mw.message( 'mwe-upwiz-license-external-invalid', 'Flickr', licenseName ).parseDom();
 		} else {
 			licenseMessage = mw.msg( 'mwe-upwiz-license-external', 'Flickr', licenseName );
 		}
diff --git a/resources/mw.UploadWizardDetails.js b/resources/mw.UploadWizardDetails.js
index 1edde964..70cffc51 100644
--- a/resources/mw.UploadWizardDetails.js
+++ b/resources/mw.UploadWizardDetails.js
@@ -312,7 +312,7 @@
 					new OO.ui.IconWidget( { icon: 'expand' } ).$element,
 					new OO.ui.IconWidget( { icon: 'collapse' } ).$element,
 					' ',
-					mw.message( 'mwe-upwiz-other-v2', mw.user ).text()
+					mw.message( 'mwe-upwiz-other-v2', mw.user ).escaped()
 				),
 				classes: [
 					'mwe-upwiz-fieldLayout-additional-info',
@@ -1594,7 +1594,7 @@
 		 */
 		showError: function ( code, html ) {
 			this.showIndicator( 'error' );
-			this.setStatus( html );
+			this.setStatus( Object.assign( html ) );
 		},
 
 		/**
@@ -1650,7 +1650,13 @@
 		},
 
 		setStatus: function ( s ) {
-			this.$div.find( '.mwe-upwiz-file-status-line' ).html( s ).show();
+			const $statusLine = this.$div.find( '.mwe-upwiz-file-status-line' );
+			if ( typeof s === 'object' ) {
+				$statusLine.html( s );
+			} else {
+				$statusLine.text( s );
+			}
+			$statusLine.show();
 		},
 
 		// TODO: De-duplicate with code form mw.UploadWizardUploadInterface.js
diff --git a/resources/mw.errorDialog.js b/resources/mw.errorDialog.js
index eacb81eb..ee34f0df 100644
--- a/resources/mw.errorDialog.js
+++ b/resources/mw.errorDialog.js
@@ -12,7 +12,7 @@
 	 */
 	mw.errorDialog = function ( errorMessage, title ) {
 		OO.ui.getWindowManager().openWindow( 'upwizErrorDialog', {
-			message: new OO.ui.HtmlSnippet( errorMessage ),
+			message: typeof errorMessage === 'object' ? new OO.ui.HtmlSnippet( errorMessage ) : errorMessage,
 			title: title || mw.message( 'mwe-upwiz-errordialog-title' ).text(),
 			verbose: true,
 			actions: [
diff --git a/resources/ui/steps/uw.ui.Upload.js b/resources/ui/steps/uw.ui.Upload.js
index f33e4aae..f71d997e 100644
--- a/resources/ui/steps/uw.ui.Upload.js
+++ b/resources/ui/steps/uw.ui.Upload.js
@@ -477,7 +477,7 @@
 	 * @param {string} filename
 	 */
 	uw.ui.Upload.prototype.showUnparseableFilenameError = function ( filename ) {
-		this.showFilenameError( mw.message( 'mwe-upwiz-unparseable-filename', filename ).escaped() );
+		this.showFilenameError( mw.msg( 'mwe-upwiz-unparseable-filename', filename ) );
 	};
 
 	/**
-- 
2.51.1

