This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/branch-4.x by this push:
new bec12bf8791a [MINOR][UI] Escape HTML when rendering error messages in
the Web UI tables
bec12bf8791a is described below
commit bec12bf8791abec093ff530a1bfa69b8d114bdd7
Author: Holden Karau <[email protected]>
AuthorDate: Thu Jul 2 13:42:25 2026 -0700
[MINOR][UI] Escape HTML when rendering error messages in the Web UI tables
### What changes were proposed in this pull request?
Task error messages and stage failure reasons are rendered by the Web UI
JavaScript (the stage-page task table, and app names on the history page) by
concatenating the text directly into HTML. Error text that contains HTML
metacharacters (for example generic type signatures such as Seq<Int>, or other
angle brackets) is therefore rendered incorrectly and can break the surrounding
table markup.
### Why are the changes needed?
Table formatting can break
### Does this PR introduce _any_ user-facing change?
Not really (fixes a usable visible bug with table formatting)
### How was this patch tested?
New test added.
### Was this patch authored or co-authored using generative AI tooling?
Claude 4.8 primarily
Backport of bced71369c73f3c2e5980c6be2dc2a64bcab74db
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Co-Authored-By: Holden Karau <[email protected]>
Co-Authored-By: Holden Karau <[email protected]>
---
.../org/apache/spark/ui/static/historypage.js | 14 ++------------
.../main/resources/org/apache/spark/ui/static/utils.js | 18 +++++++++++++++---
ui-test/tests/utils.test.js | 14 ++++++++++++++
3 files changed, 31 insertions(+), 15 deletions(-)
diff --git a/core/src/main/resources/org/apache/spark/ui/static/historypage.js
b/core/src/main/resources/org/apache/spark/ui/static/historypage.js
index 48f3ce77498c..7583b99fc4e3 100644
--- a/core/src/main/resources/org/apache/spark/ui/static/historypage.js
+++ b/core/src/main/resources/org/apache/spark/ui/static/historypage.js
@@ -17,7 +17,7 @@
/* global $, jQuery, uiRoot */
-import {formatDuration, formatTimeMillis, stringAbbreviate} from "./utils.js";
+import {escapeHtml, formatDuration, formatTimeMillis, stringAbbreviate} from
"./utils.js";
export {setAppLimit};
@@ -27,17 +27,7 @@ var appLimit = -1;
function setAppLimit(val) {
appLimit = val;
}
-/* escape XSS */
-function escapeHtml(text) {
- if (typeof text !== 'string') return text;
- return text
- .replace(/&/g, "&")
- .replace(/</g, "<")
- .replace(/>/g, ">")
- .replace(/"/g, """)
- .replace(/'/g, "'");
-}
-/* eslint-enable no-unused-vars*/
+/* eslint-enable no-unused-vars */
function makeIdNumeric(id) {
var strs = id.split("_");
diff --git a/core/src/main/resources/org/apache/spark/ui/static/utils.js
b/core/src/main/resources/org/apache/spark/ui/static/utils.js
index ca5fbdc6b80c..3f0885b9c4f5 100644
--- a/core/src/main/resources/org/apache/spark/ui/static/utils.js
+++ b/core/src/main/resources/org/apache/spark/ui/static/utils.js
@@ -17,7 +17,7 @@
export {
ConvertDurationString, createRESTEndPointForExecutorsPage,
createRESTEndPointForMiscellaneousProcess, createTemplateURI,
- errorMessageCell, errorSummary,
+ errorMessageCell, errorSummary, escapeHtml,
formatBytes, formatDate, formatDuration, formatLogsCells, formatTimeMillis,
getBaseURI, getStandAloneAppId, getTimeZone,
setDataTableDefaults, stringAbbreviate
@@ -235,6 +235,18 @@ function getBaseURI() {
return document.baseURI || document.URL;
}
+/* Escape a value for safe inclusion in HTML text content. Non-string values
are
+ * coerced to a string first; null/undefined are returned unchanged. */
+function escapeHtml(text) {
+ if (text === null || text === undefined) return text;
+ return String(text)
+ .replace(/&/g, "&")
+ .replace(/</g, "<")
+ .replace(/>/g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+}
+
function detailsUINode(isMultiline, message) {
if (isMultiline) {
const span = '<span data-toggle-details=".stacktrace-details"
class="expand-details">+details</span>';
@@ -269,8 +281,8 @@ function errorSummary(errorMessage) {
function errorMessageCell(errorMessage) {
const [summary, isMultiline] = errorSummary(errorMessage);
- const details = detailsUINode(isMultiline, errorMessage);
- return summary + details;
+ const details = detailsUINode(isMultiline, escapeHtml(errorMessage));
+ return escapeHtml(summary) + details;
}
function stringAbbreviate(content, limit) {
diff --git a/ui-test/tests/utils.test.js b/ui-test/tests/utils.test.js
index a6815577bd82..b49f5034ff72 100644
--- a/ui-test/tests/utils.test.js
+++ b/ui-test/tests/utils.test.js
@@ -74,3 +74,17 @@ test('stringAbbreviate', function () {
expect(utils.stringAbbreviate('12345678901', 10)).toContain('1234567890...');
expect(utils.stringAbbreviate('12345678901',
10)).toContain('<pre>12345678901</pre>')
});
+
+test('errorMessageCell escapes HTML in error messages', function () {
+ const payload = '<img src=x onerror=alert(document.domain)>';
+
+ // A single-line message becomes the summary cell; it must be HTML-escaped.
+ const singleLine = utils.errorMessageCell(payload);
+ expect(singleLine).not.toContain(payload);
+ expect(singleLine).toContain('<img src=x
onerror=alert(document.domain)>');
+
+ // A multi-line message puts the payload in the expandable <pre> details.
+ const multiLine = utils.errorMessageCell('boom\n' + payload);
+ expect(multiLine).not.toContain(payload);
+ expect(multiLine).toContain('<img src=x
onerror=alert(document.domain)>');
+});
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]