From 0bcbb77fd558ba80782a272416d1decc44c3b9c8 Mon Sep 17 00:00:00 2001
From: SomeRandomDeveloper <thisisnotmyname275@gmail.com>
Date: Mon, 23 Feb 2026 23:21:17 +0100
Subject: [PATCH] SECURITY: Fix multiple XSS vulnerabilities

* Escape site URL in GlobalWatchlistLinker.fixLocalLinks and use a
  replacement function since it's generally safer to be able to
  control where the replacement occurs
* Escape or parse system messages where necessary
* Use mw.html.element() instead of concatenating HTML strings

Bug: T418179
Change-Id: I1fc7b7e1d234b0aaf9f7d782a65da1451577587e
---
 modules/specialglobalwatchlist/EntryBase.js      |  2 +-
 modules/specialglobalwatchlist/Linker.js         |  2 +-
 modules/specialglobalwatchlist/SiteBase.js       |  2 +-
 modules/specialglobalwatchlist/SiteDisplay.js    |  3 +--
 modules/specialglobalwatchlist/WatchlistUtils.js | 13 +++++++------
 5 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/modules/specialglobalwatchlist/EntryBase.js b/modules/specialglobalwatchlist/EntryBase.js
index 6b3aed8..9d307fd 100644
--- a/modules/specialglobalwatchlist/EntryBase.js
+++ b/modules/specialglobalwatchlist/EntryBase.js
@@ -64,7 +64,7 @@ function GlobalWatchlistEntryBase( info ) {
 	this.commentDisplay = info.commentDisplay;
 
 	/**
-	 * @property {string|boolean} tagsDisplay Either `false` or a raw HTML string for the
+	 * @property {jQuery|boolean} tagsDisplay Either `false` or a jQuery object for the
 	 *   parsed tags information that should be shown
 	 */
 	this.tagsDisplay = info.tagsDisplay;
diff --git a/modules/specialglobalwatchlist/Linker.js b/modules/specialglobalwatchlist/Linker.js
index fc06426..6a13b68 100644
--- a/modules/specialglobalwatchlist/Linker.js
+++ b/modules/specialglobalwatchlist/Linker.js
@@ -26,7 +26,7 @@ GlobalWatchlistLinker.prototype.fixLocalLinks = function ( comment ) {
 	// use /wiki/$1 and /w/index.php?
 	return comment.replace(
 		/<a href="(\/wiki\/|\/w\/index\.php\?)/g,
-		'<a href="//' + this.site + '$1'
+		( _, p1 ) => '<a href="//' + mw.html.escape( this.site ) + p1
 	);
 };
 
diff --git a/modules/specialglobalwatchlist/SiteBase.js b/modules/specialglobalwatchlist/SiteBase.js
index 908aae6..61c0dad 100644
--- a/modules/specialglobalwatchlist/SiteBase.js
+++ b/modules/specialglobalwatchlist/SiteBase.js
@@ -98,7 +98,7 @@ GlobalWatchlistSiteBase.prototype.api = function ( func, content, name ) {
 
 			const $userNotification = $( '<div>' )
 				.append(
-					mw.msg( 'globalwatchlist-api-error', that.site ),
+					mw.message( 'globalwatchlist-api-error', that.site ).escaped(),
 					that.apiObject.getErrorMessage( data )
 				);
 
diff --git a/modules/specialglobalwatchlist/SiteDisplay.js b/modules/specialglobalwatchlist/SiteDisplay.js
index 00db59c..b5db86a 100644
--- a/modules/specialglobalwatchlist/SiteDisplay.js
+++ b/modules/specialglobalwatchlist/SiteDisplay.js
@@ -217,10 +217,9 @@ GlobalWatchlistSiteDisplay.prototype.makePageLink = function ( entry ) {
 	}
 
 	if ( entry.tagsDisplay ) {
-		// Need to process links in the parsed description as raw HTML
 		const $tags = $( '<em>' )
 			.addClass( 'ext-globalwatchlist-tags' )
-			.html( entry.tagsDisplay );
+			.append( entry.tagsDisplay );
 		$row.append( ' ' )
 			.append( $tags );
 	}
diff --git a/modules/specialglobalwatchlist/WatchlistUtils.js b/modules/specialglobalwatchlist/WatchlistUtils.js
index 6153453..51558d0 100644
--- a/modules/specialglobalwatchlist/WatchlistUtils.js
+++ b/modules/specialglobalwatchlist/WatchlistUtils.js
@@ -139,10 +139,11 @@ GlobalWatchlistWatchlistUtils.prototype.makeUserLinks = function ( editsByUser )
 				'Special:Contributions/' :
 				'User:';
 			userLinkURL = that.linker.linkPage( userLinkBase + userMessage );
-			userLink = '<a href="' + userLinkURL + '" target="_blank" class="mw-userlink' +
-				// styling anonymous users and temporary accounts names
-				( editsByUser[ userMessage ].temp ? ' mw-tempuserlink' : '' ) +
-				'"><bdi>' + userMessage + '</bdi></a>';
+			userLink = mw.html.element( 'a', {
+				href: userLinkURL,
+				target: '_blank',
+				class: 'mw-userlink' + ( editsByUser[ userMessage ].temp ? ' mw-tempuserlink' : '' )
+			}, new mw.html.Raw( mw.html.element( 'bdi', {}, userMessage ) ) );
 		}
 		if ( editsByUser[ userMessage ].editCount > 1 ) {
 			userLink = userLink + ' ' +
@@ -398,8 +399,8 @@ GlobalWatchlistWatchlistUtils.prototype.getFinalEntries = function (
 				}
 				return $acc.append( $( '<bdi>' ).html( that.linker.newTabLinks( tagsInfo[ tagName ] ) ) );
 			}, $( '<span>' ) );
-			$tagsWithLabel = mw.msg( 'globalwatchlist-tags', entry.tags.length, $tagDescriptions.html() );
-			entry.tagsDisplay = mw.msg( 'parentheses', $tagsWithLabel );
+			$tagsWithLabel = mw.message( 'globalwatchlist-tags', entry.tags.length, $tagDescriptions ).parseDom();
+			entry.tagsDisplay = mw.message( 'parentheses', $tagsWithLabel ).parseDom();
 		}
 
 		// Comment display
-- 
2.53.0

