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 += '
' + logName + '
' + // Custom log names and URLs can contain markup characters. + if (isHttpOrRelativeUrl(logUrl)) { + result += '
' + escapeHtml(logName) + '
' + } 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( + '
<b>stdout</b>
'); + + // 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('
stderr
'); + expect(utils.formatLogsCells({'stdout': 'https://worker:8081/logPage'}, 'display')) + .toBe('
stdout
'); + + // Non-display rendering returns the log names for sorting and filtering. + expect(utils.formatLogsCells({'stdout': 'javascript:alert(1)'}, 'sort')).toEqual(['stdout']); +});