From a232c534a6e6d09b3ba9c0b43fbe8eefd5568a97 Mon Sep 17 00:00:00 2001
From: "James D. Forrester" <jforrester@wikimedia.org>
Date: Thu, 11 Sep 2025 17:03:05 -0400
Subject: [PATCH] SECURITY: Do not let getErrorMessages() return HTML ever, at
 least for now

This needs a proper fix, but this will prevent damage, at least.

Bug: T404392
Change-Id: I76ba6168e9723230f95f5a2785df90f9327a0148
---
 .../components/base/ZObjectSelector.vue       |  3 +--
 .../components/types/ZArgumentReference.vue   |  3 +--
 .../components/types/ZCode.vue                |  6 ++----
 .../types/ZObjectStringRenderer.vue           |  3 +--
 .../visualeditor/FunctionInputField.vue       |  3 +--
 .../FunctionMetadataDialog.vue                |  3 +--
 .../widgets/publish/PublishDialog.vue         |  3 +--
 .../ext.wikilambda.app/mixins/errorMixin.js   |  9 +++++++--
 .../widgets/publish/PublishDialog.test.js     | 19 +++++++++++++++++++
 9 files changed, 34 insertions(+), 18 deletions(-)

diff --git a/resources/ext.wikilambda.app/components/base/ZObjectSelector.vue b/resources/ext.wikilambda.app/components/base/ZObjectSelector.vue
index 70bcaad0..bb6f4ee6 100644
--- a/resources/ext.wikilambda.app/components/base/ZObjectSelector.vue
+++ b/resources/ext.wikilambda.app/components/base/ZObjectSelector.vue
@@ -51,8 +51,7 @@
 				:type="error.type"
 				:inline="true"
 			>
-				<!-- eslint-disable vue/no-v-html -->
-				<div v-html="getErrorMessage( error )"></div>
+				<div>{{ getErrorMessage( error ) }}</div>
 			</cdx-message>
 		</div>
 	</span>
diff --git a/resources/ext.wikilambda.app/components/types/ZArgumentReference.vue b/resources/ext.wikilambda.app/components/types/ZArgumentReference.vue
index d07ebcf7..37fc27ae 100644
--- a/resources/ext.wikilambda.app/components/types/ZArgumentReference.vue
+++ b/resources/ext.wikilambda.app/components/types/ZArgumentReference.vue
@@ -34,8 +34,7 @@
 				:type="error.type"
 				:inline="true"
 			>
-				<!-- eslint-disable vue/no-v-html -->
-				<div v-html="getErrorMessage( error )"></div>
+				<div>{{ getErrorMessage( error ) }}</div>
 			</cdx-message>
 		</div>
 	</div>
diff --git a/resources/ext.wikilambda.app/components/types/ZCode.vue b/resources/ext.wikilambda.app/components/types/ZCode.vue
index 69f8c973..30ac3cb0 100644
--- a/resources/ext.wikilambda.app/components/types/ZCode.vue
+++ b/resources/ext.wikilambda.app/components/types/ZCode.vue
@@ -42,8 +42,7 @@
 					:type="error.type"
 					:inline="true"
 				>
-					<!-- eslint-disable-next-line vue/no-v-html -->
-					<div v-html="getErrorMessage( error )"></div>
+					<div>{{ getErrorMessage( error ) }}</div>
 				</cdx-message>
 			</template>
 		</wl-key-value-block>
@@ -79,8 +78,7 @@
 					:type="error.type"
 					:inline="true"
 				>
-					<!-- eslint-disable-next-line vue/no-v-html -->
-					<div v-html="getErrorMessage( error )"></div>
+					<div>{{ getErrorMessage( error ) }}</div>
 				</cdx-message>
 			</template>
 		</wl-key-value-block>
diff --git a/resources/ext.wikilambda.app/components/types/ZObjectStringRenderer.vue b/resources/ext.wikilambda.app/components/types/ZObjectStringRenderer.vue
index b69a50c3..0c6ca02f 100644
--- a/resources/ext.wikilambda.app/components/types/ZObjectStringRenderer.vue
+++ b/resources/ext.wikilambda.app/components/types/ZObjectStringRenderer.vue
@@ -31,8 +31,7 @@
 				class="ext-wikilambda-app-object-string-renderer__error"
 			>
 				<cdx-message :type="fieldErrors[0].type" :inline="true">
-					<!-- eslint-disable vue/no-v-html -->
-					<span v-html="getErrorMessage( fieldErrors[0] )"></span>
+					<span>{{ getErrorMessage( fieldErrors[0] ) }}</span>
 				</cdx-message>
 				<a v-if="showExamplesLink" @click="openExamplesDialog">
 					{{ $i18n( 'wikilambda-string-renderer-examples-title' ).text() }}
diff --git a/resources/ext.wikilambda.app/components/visualeditor/FunctionInputField.vue b/resources/ext.wikilambda.app/components/visualeditor/FunctionInputField.vue
index 74acbb8a..060d92a2 100644
--- a/resources/ext.wikilambda.app/components/visualeditor/FunctionInputField.vue
+++ b/resources/ext.wikilambda.app/components/visualeditor/FunctionInputField.vue
@@ -32,8 +32,7 @@
 			</span>
 		</template>
 		<template v-if="showValidation && !!errorMessage" #error>
-			<!-- eslint-disable-next-line vue/no-v-html -->
-			<span v-html="errorMessage"></span>
+			<div>{{ getErrorMessage( error ) }}</div>
 		</template>
 	</cdx-field>
 </template>
diff --git a/resources/ext.wikilambda.app/components/widgets/function-evaluator/FunctionMetadataDialog.vue b/resources/ext.wikilambda.app/components/widgets/function-evaluator/FunctionMetadataDialog.vue
index d41f1540..6c0ee9f3 100644
--- a/resources/ext.wikilambda.app/components/widgets/function-evaluator/FunctionMetadataDialog.vue
+++ b/resources/ext.wikilambda.app/components/widgets/function-evaluator/FunctionMetadataDialog.vue
@@ -44,8 +44,7 @@
 				class="ext-wikilambda-metadata-dialog-errors"
 				:type="error.type"
 			>
-				<!-- eslint-disable vue/no-v-html -->
-				<div v-html="getErrorMessage( error )"></div>
+				<div>{{ getErrorMessage( error ) }}</div>
 			</cdx-message>
 		</div>
 		<div v-else class="ext-wikilambda-app-function-metadata-dialog__body">
diff --git a/resources/ext.wikilambda.app/components/widgets/publish/PublishDialog.vue b/resources/ext.wikilambda.app/components/widgets/publish/PublishDialog.vue
index cf2b7f16..a08f1f5f 100644
--- a/resources/ext.wikilambda.app/components/widgets/publish/PublishDialog.vue
+++ b/resources/ext.wikilambda.app/components/widgets/publish/PublishDialog.vue
@@ -30,8 +30,7 @@
 					class="ext-wikilambda-app-publish-dialog__error"
 					:type="error.type"
 				>
-					<!-- eslint-disable vue/no-v-html -->
-					<div v-html="getErrorMessage( error )"></div>
+					<div>{{ getErrorMessage( error ) }}</div>
 				</cdx-message>
 			</div>
 
diff --git a/resources/ext.wikilambda.app/mixins/errorMixin.js b/resources/ext.wikilambda.app/mixins/errorMixin.js
index 8afa30da..9cb57eee 100644
--- a/resources/ext.wikilambda.app/mixins/errorMixin.js
+++ b/resources/ext.wikilambda.app/mixins/errorMixin.js
@@ -34,15 +34,20 @@ module.exports = exports = {
 
 		/**
 		 * Returns the translated message for a given error code.
-		 * Error messages can have html tags.
+		 * Raw error messages cannot have HTML tags, as they are escaped.
 		 *
 		 * @memberof module:ext.wikilambda.app.mixins.errorMixin
 		 * @param {Object} error
 		 * @return {string}
 		 */
 		getErrorMessage: function ( error ) {
+			if ( error.message ) {
+				const div = document.createElement( 'div' );
+				div.appendChild( document.createTextNode( error.message ) );
+				return div.innerHTML;
+			}
 			// eslint-disable-next-line mediawiki/msg-doc
-			return error.message || this.$i18n( error.code ).parse();
+			return this.$i18n( error.code ).parse();
 		},
 
 		/**
diff --git a/tests/jest/components/widgets/publish/PublishDialog.test.js b/tests/jest/components/widgets/publish/PublishDialog.test.js
index e5eeb978..d3b4d943 100644
--- a/tests/jest/components/widgets/publish/PublishDialog.test.js
+++ b/tests/jest/components/widgets/publish/PublishDialog.test.js
@@ -91,6 +91,25 @@ describe( 'Publish Dialog', () => {
 		expect( messages[ 1 ].text() ).toBe( 'Unable to complete request. Please try again.' );
 	} );
 
+	it( 'renders escaped page errors', () => {
+		const errors = [ {
+			message: "<button onmouseover=\"window.location = '//www.example.com'\">",
+			code: undefined,
+			type: Constants.ERROR_TYPES.ERROR
+		} ];
+		store.getErrors = createGettersWithFunctionsMock( errors );
+
+		const wrapper = mount( PublishDialog, {
+			props: { showDialog: true },
+			global: { stubs: dialogGlobalStubs }
+		} );
+
+		const messages = wrapper.findAllComponents( { name: 'cdx-message' } );
+		expect( messages.length ).toBe( 1 );
+		expect( messages[ 0 ].props( 'type' ) ).toBe( 'error' );
+		expect( messages[ 0 ].text() ).toBe( '&lt;button onmouseover=\"window.location = \'//www.example.com\'\"&gt;' );
+	} );
+
 	it( 'closes the dialog when click cancel button', () => {
 		const wrapper = mount( PublishDialog, {
 			props: { showDialog: true },
-- 
2.39.5 (Apple Git-154)

