From 9cc4b19340783d4f5b4f0cd09bc3cdc340616c21 Mon Sep 17 00:00:00 2001
From: Matthias Mullie <git@mullie.eu>
Date: Fri, 7 Mar 2025 11:39:46 +0100
Subject: [PATCH] SECURITY: Fix XSS vulnerability
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Attributes prefixed data-mw- are covered by Sanitizer.php and should be
safe to use. Others are not safe, as they can also be used in Wikitext.

For data-(mw-)property and data-(mw-)statements, fall back to the old
attribute value for cached cases where the new ones are missing. This is
hopefully relatively safe – Wikitext with custom values for these
attributes may be able to cause confusing on-page behavior, but
hopefully not code execution. To further safeguard this, make the
selector more precise – this should hopefully cut out most
user-generated content (though discussion on the task is inconclusive,
as of this writing, whether it’s fully enough or not).

For data-(mw-)formatvalue, don’t fall back to the old values. Values of
this attribute are directly used as HTML and are thus highly unsafe; if
we can’t use them, the wbformatvalue cache should just fall back to
getting them from the API, which is slightly less efficient but safe.

Bug: T387691
Change-Id: I18122bf29a53b963f0919051abe0e9254a4c4db2
---
 resources/filepage/StatementPanel.js          |  4 ++--
 resources/filepage/init.js                    | 21 +++++++++++++------
 .../inputs/EntityAutocompleteInputWidget.js   |  2 +-
 src/View/MediaInfoEntityStatementsView.php    | 11 +++-------
 ...yAutocompleteInputWidgetLabel.mustache+dom |  2 +-
 5 files changed, 22 insertions(+), 18 deletions(-)

diff --git a/resources/filepage/StatementPanel.js b/resources/filepage/StatementPanel.js
index 07445fbf3b..cac0bdb49b 100644
--- a/resources/filepage/StatementPanel.js
+++ b/resources/filepage/StatementPanel.js
@@ -37,8 +37,8 @@ StatementPanel = function StatementPanelConstructor( config ) {
 	// Mixin constructors
 	OO.ui.mixin.PendingElement.call( this, this.config );
 
-	if ( this.$element.attr( 'data-formatvalue' ) ) {
-		this.populateFormatValueCache( JSON.parse( this.$element.attr( 'data-formatvalue' ) || '{}' ) );
+	if ( this.$element.attr( 'data-mw-formatvalue' ) ) {
+		this.populateFormatValueCache( JSON.parse( this.$element.attr( 'data-mw-formatvalue' ) || '{}' ) );
 	}
 
 	this.licenseDialogWidget = new LicenseDialogWidget();
diff --git a/resources/filepage/init.js b/resources/filepage/init.js
index 3eed8a5f65..ec4d44de8a 100644
--- a/resources/filepage/init.js
+++ b/resources/filepage/init.js
@@ -175,7 +175,7 @@
 	mw.hook( 'wikipage.content' ).add( function ( $content ) {
 		var linkNoticeWidget = new LinkNoticeWidget(),
 			protectionMsgWidget = new ProtectionMsgWidget(),
-			$statements = $content.find( '.wbmi-entityview-statementsGroup' ),
+			$statements = $content.find( '.wbmi-structured-data-header ~ .wbmi-entityview-statementsGroup' ),
 			existingProperties = defaultProperties.concat( Object.keys( mediaInfoEntity.statements || {} ) ),
 			deserializer = new StatementListDeserializer(),
 			tabs,
@@ -200,7 +200,7 @@
 		$content.find( '.wbmi-tabs-container' ).first().before( protectionMsgWidget.$element );
 		captionsPanel = createCaptionsPanel();
 		// eslint-disable-next-line no-jquery/no-global-selector
-		$( '.wbmi-entityview-captionsPanel' ).replaceWith( captionsPanel.$element );
+		$( '.wbmi-captions-header ~ .wbmi-entityview-captionsPanel' ).replaceWith( captionsPanel.$element );
 
 		// Add link notice widget and add property button if they don't exist.
 		if ( $statements.first().hasClass( 'wbmi-entityview-statementsGroup' ) ) {
@@ -212,10 +212,19 @@
 		}
 
 		// Set up existing statement panels
-		existingStatementPanels = $statements.get().map( function ( element ) {
-			var $statement = $( element ),
-				propId = $statement.data( 'property' ),
-				statementsJson = JSON.parse( $statement.attr( 'data-statements' ) || '[]' ),
+		existingStatementPanels = $statements.get().map( ( element ) => {
+			const $statement = $( element ),
+				propId = $statement.data( 'mw-property' ) ||
+					// Fallback for when this attribute was named differently
+					// @see https://phabricator.wikimedia.org/T387691
+					$statement.data( 'property' ),
+				statementsJson = JSON.parse(
+					$statement.attr( 'data-mw-statements' ) ||
+					// Fallback for when this attribute was named differently
+					// @see https://phabricator.wikimedia.org/T387691
+					$statement.attr( 'data-statements' ) ||
+					'[]'
+				),
 				data = deserializer.deserialize( statementsJson ),
 				statementPanel = createStatementsPanel(
 					$statement,
diff --git a/resources/statements/inputs/EntityAutocompleteInputWidget.js b/resources/statements/inputs/EntityAutocompleteInputWidget.js
index a09c865b39..7b76bd59de 100644
--- a/resources/statements/inputs/EntityAutocompleteInputWidget.js
+++ b/resources/statements/inputs/EntityAutocompleteInputWidget.js
@@ -216,7 +216,7 @@ EntityAutocompleteInputWidget.prototype.onMousedown = function ( e ) {
 		// window.open. This is a response to a mousedown event so it shouldn't
 		// trigger any popup blockers in modern browsers. For browsers set to
 		// prefer new tabs over new windows, this will open in a new tab.
-		window.open( e.currentTarget.dataset.url, '_blank' );
+		window.open( e.currentTarget.dataset.mwUrl, '_blank' );
 	}
 };
 
diff --git a/src/View/MediaInfoEntityStatementsView.php b/src/View/MediaInfoEntityStatementsView.php
index 0e6df996da..2ca522ef69 100644
--- a/src/View/MediaInfoEntityStatementsView.php
+++ b/src/View/MediaInfoEntityStatementsView.php
@@ -261,9 +261,9 @@ private function getLayoutForProperty( $propertyIdString, array $statements ) {
 		] );
 		$panel->setAttributes(
 			[
-				'data-property' => $propertyIdString,
-				'data-statements' => json_encode( $serializedStatements ),
-				'data-formatvalue' => json_encode( $formatValueCache ),
+				'data-mw-property' => $propertyIdString,
+				'data-mw-statements' => json_encode( $serializedStatements ),
+				'data-mw-formatvalue' => json_encode( $formatValueCache ),
 			]
 		);
 		return $panel;
@@ -350,11 +350,6 @@ private function innerStatementDiv( Statement $statement ): Tag {
 		$statementDiv = new Tag( 'div' );
 		$statementDiv->addClasses( [ 'wbmi-item-container' ] );
 
-		$guid = $statement->getGuid();
-		if ( $guid !== null ) {
-			$statementDiv->setAttributes( [ 'data-guid' => $guid ] );
-		}
-
 		$mainSnakDiv = new Tag( 'div' );
 		$mainSnakDiv->addClasses( [ 'wbmi-entity-header' ] );
 		$mainSnakDiv->appendContent(
diff --git a/templates/statements/inputs/EntityAutocompleteInputWidgetLabel.mustache+dom b/templates/statements/inputs/EntityAutocompleteInputWidgetLabel.mustache+dom
index 7320a630f0..7795747737 100644
--- a/templates/statements/inputs/EntityAutocompleteInputWidgetLabel.mustache+dom
+++ b/templates/statements/inputs/EntityAutocompleteInputWidgetLabel.mustache+dom
@@ -1,4 +1,4 @@
-<span class="wbmi-autocomplete-option" data-url={{url}}>
+<span class="wbmi-autocomplete-option" data-mw-url={{url}}>
 	<span class="wbmi-autocomplete-option__label">
 		{{label}}
 	</span>
-- 
2.48.1

