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 08701c86fa21e..bf4ff15de8787 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 @@ -85,12 +85,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; } @@ -231,4 +247,16 @@ function createRESTEndPointForMiscellaneousProcess(appId) { 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, "'"); +} /* eslint-enable no-unused-vars */ \ No newline at end of file