From 2ef50ee43db7d60a8b3df6ad7b4641631f521f0a Mon Sep 17 00:00:00 2001 From: SomeRandomDeveloper 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', `

Examples of good file names:


Examples of bad file names:

[https://commons.wikimedia.org/wiki/MediaWiki:Titleblacklist-custom-filename Learn more]

` ); + } 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