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 3f0885b9c4f58..bc3143fa4efec 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
@@ -88,12 +88,28 @@ function getTimeZone() {
}
}
+/* Custom log URLs are rendered as links only when they are http(s) or relative
+ * URLs; anything else is shown as plain text. Browsers strip ASCII whitespace
+ * and control characters when parsing URLs, so remove them before checking
+ * the scheme. */
+function isHttpOrRelativeUrl(url) {
+ if (typeof url !== 'string') return false;
+ /* eslint-disable-next-line no-control-regex */
+ var normalized = url.replace(/[\u0000-\u0020]/g, '').toLowerCase();
+ return /^https?:\/\//.test(normalized) || !/^[a-z][a-z0-9+.-]*:/.test(normalized);
+}
+
function formatLogsCells(execLogs, type) {
if (type !== 'display') return Object.keys(execLogs);
if (!execLogs) return;
var result = '';
$.each(execLogs, function (logName, logUrl) {
- result += '
'
+ // Custom log names and URLs can contain markup characters.
+ if (isHttpOrRelativeUrl(logUrl)) {
+ result += ''
+ } else {
+ result += '' + escapeHtml(logName) + '
'
+ }
});
return result;
}
diff --git a/ui-test/tests/utils.test.js b/ui-test/tests/utils.test.js
index b49f5034ff72a..a2774475bf4f5 100644
--- a/ui-test/tests/utils.test.js
+++ b/ui-test/tests/utils.test.js
@@ -16,6 +16,7 @@
*/
+import '../../core/src/main/resources/org/apache/spark/ui/static/jquery.min.js';
import * as utils from '../../core/src/main/resources/org/apache/spark/ui/static/utils.js';
test('ConvertDurationString', function () {
@@ -88,3 +89,24 @@ test('errorMessageCell escapes HTML in error messages', function () {
expect(multiLine).not.toContain(payload);
expect(multiLine).toContain('<img src=x onerror=alert(document.domain)>');
});
+
+test('formatLogsCells escapes custom log names and URLs', function () {
+ // Names and URLs are escaped, and the href attribute is quoted.
+ const rendered = utils.formatLogsCells(
+ {'stdout': 'http://worker:8081/log?a=1&b="2"'}, 'display');
+ expect(rendered).toBe(
+ '');
+
+ // Only http(s) and relative URLs become links; other schemes render as text.
+ ['javascript:alert(1)', 'JAVAS\tCRIPT:alert(1)', 'data:text/html,x'].forEach(function (url) {
+ expect(utils.formatLogsCells({'stdout': url}, 'display')).toBe('stdout
');
+ });
+
+ expect(utils.formatLogsCells({'stderr': 'logPage/?self&logType=stderr'}, 'display'))
+ .toBe('');
+ expect(utils.formatLogsCells({'stdout': 'https://worker:8081/logPage'}, 'display'))
+ .toBe('');
+
+ // Non-display rendering returns the log names for sorting and filtering.
+ expect(utils.formatLogsCells({'stdout': 'javascript:alert(1)'}, 'sort')).toEqual(['stdout']);
+});