This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/master by this push:
new 432adc74d4ca [MINOR][UI] Escape HTML when rendering error messages in
the Web UI tables
432adc74d4ca is described below
commit 432adc74d4ca01715d59e8f10cef1e732d77649e
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
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.
Table formatting can break
Not really (fixes a usable visible bug with table formatting)
New test added.
Claude 4.8 primarily
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]