From bb5ea994934b2b84877f302e1bf2a6a686a67a37 Mon Sep 17 00:00:00 2001
From: SomeRandomDeveloper <thisisnotmyname275@gmail.com>
Date: Wed, 4 Feb 2026 18:46:43 +0100
Subject: [PATCH] SECURITY: Sanitize URLs and use jQuery to build links

* Sanitize 'commonsUrl' and 'termsUrl' in PHP and pass it to the JS
  through the startup data.
* Build link elements using jQuery and render the messages using the
  vue-i18n-html directive instead of using mw.message, string
  replacements and v-html.

This fixes multiple i18n XSS vulnerabilities.

Bug: T416502
Change-Id: Iab86209478a044504f5a6aea0d8c3d14f21c48b3
---
 includes/Hooks.php                            |  6 ++++
 .../ext.wikiLove.startup/WikiLoveDialog.vue   | 32 ++++++++-----------
 resources/ext.wikiLove.startup/wikiLove.js    | 11 -------
 3 files changed, 19 insertions(+), 30 deletions(-)

diff --git a/includes/Hooks.php b/includes/Hooks.php
index 1dc2f99..518bdc8 100644
--- a/includes/Hooks.php
+++ b/includes/Hooks.php
@@ -237,6 +237,12 @@ class Hooks implements
 			'whatIsThisLink' => Skin::makeInternalOrExternalUrl(
 				$context->msg( 'wikilove-what-is-this-link' )->text()
 			),
+			'commonsUrl' => Skin::makeInternalOrExternalUrl(
+				$context->msg( 'wikilove-commons-url' )->text()
+			),
+			'termsUrl' => Skin::makeInternalOrExternalUrl(
+				$context->msg( 'wikilove-terms-url' )->text()
+			),
 		];
 	}
 
diff --git a/resources/ext.wikiLove.startup/WikiLoveDialog.vue b/resources/ext.wikiLove.startup/WikiLoveDialog.vue
index e827cb8..8862f71 100644
--- a/resources/ext.wikiLove.startup/WikiLoveDialog.vue
+++ b/resources/ext.wikiLove.startup/WikiLoveDialog.vue
@@ -72,8 +72,7 @@
 							class="text"
 							@change="onImageChange"
 						>
-						<!-- eslint-disable-next-line vue/no-v-html -->
-						<div id="mw-wikilove-commons-text" v-html="commonsText"></div>
+						<div id="mw-wikilove-commons-text" v-i18n-html:wikilove-commons-text="[ commonsLink ]"></div>
 						<label id="mw-wikilove-message-label" for="mw-wikilove-message">{{ $i18n( 'wikilove-enter-message' ) }}</label>
 						<span id="mw-wikilove-message-note" class="mw-wikilove-note">{{ $i18n( 'wikilove-omit-sig' ) }}</span>
 						<textarea id="mw-wikilove-message" rows="4"></textarea>
@@ -99,8 +98,7 @@
 					<span class="mw-wikilove-number">3</span>
 					<h3>{{ $i18n( 'wikilove-preview' ) }}</h3>
 					<div id="mw-wikilove-preview-area"></div>
-					<!-- eslint-disable-next-line vue/no-v-html -->
-					<div id="mw-wikilove-terms" v-html="terms"></div>
+					<div id="mw-wikilove-terms" v-i18n-html:wikilove-terms="[ termsLink ]"></div>
 					<form id="mw-wikilove-send-form">
 						<cdx-button
 							id="mw-wikilove-button-send"
@@ -119,7 +117,7 @@
 
 <script>
 const { cdxIconClose } = require( './icons.json' );
-const { whatIsThisLink } = require( './data.json' );
+const { whatIsThisLink, commonsUrl, termsUrl } = require( './data.json' );
 const { CdxIcon, CdxButton } = require( '@wikimedia/codex' );
 const Vue = require( 'vue' );
 
@@ -135,27 +133,23 @@ module.exports = Vue.defineComponent( {
 			type: String,
 			default: cdxIconClose,
 		},
-		commonsLink: {
-			type: String,
-			required: true,
-		},
-		termsLink: {
-			type: String,
-			required: true,
-		},
 	},
 	emits: [ 'close' ],
 	data: () => ( {
 		whatIsThisLink,
 	} ),
 	computed: {
-		terms() {
-			// TODO improve this logic so that v-html isn't needed
-			return mw.message( 'wikilove-terms' ).parse().replace( '$1', this.termsLink );
+		termsLink() {
+			return $( '<a>' )
+				.attr( 'href', termsUrl )
+				.attr( 'target', '_blank' )
+				.text( mw.msg( 'wikilove-terms-link' ) );
 		},
-		commonsText() {
-			// TODO improve this logic so that v-html isn't needed
-			return mw.message( 'wikilove-commons-text' ).parse().replace( '$1', this.commonsLink );
+		commonsLink() {
+			return $( '<a>' )
+				.attr( 'href', commonsUrl )
+				.attr( 'target', '_blank' )
+				.text( mw.msg( 'wikilove-commons-link' ) );
 		},
 	},
 	methods: {
diff --git a/resources/ext.wikiLove.startup/wikiLove.js b/resources/ext.wikiLove.startup/wikiLove.js
index 70a2eb1..de4dbd7 100644
--- a/resources/ext.wikiLove.startup/wikiLove.js
+++ b/resources/ext.wikiLove.startup/wikiLove.js
@@ -65,15 +65,6 @@ module.exports = {
 			$typeList.append( $( '<li>' ).append( $button ) );
 		}
 
-		const commonsLink = mw.html.element( 'a', {
-			href: mw.msg( 'wikilove-commons-url' ),
-			target: '_blank',
-		}, mw.msg( 'wikilove-commons-link' ) );
-		const termsLink = mw.html.element( 'a', {
-			href: mw.msg( 'wikilove-terms-url' ),
-			target: '_blank',
-		}, mw.msg( 'wikilove-terms-link' ) );
-
 		overlayContainer.classList.add( 'wikilove-overlay-container' );
 		overlayContainer.style.display = '';
 
@@ -83,8 +74,6 @@ module.exports = {
 		// render.
 		document.body.appendChild( overlayContainer );
 		Vue.createMwApp( WikiLoveDialog, {
-			commonsLink,
-			termsLink,
 			onClose: () => {
 				this.reset();
 			},
-- 
2.52.0

