diff --git a/core/src/main/scala/org/apache/spark/ui/UIUtils.scala b/core/src/main/scala/org/apache/spark/ui/UIUtils.scala index c0d36ef3a8acf..c1b12e29d0fbd 100644 --- a/core/src/main/scala/org/apache/spark/ui/UIUtils.scala +++ b/core/src/main/scala/org/apache/spark/ui/UIUtils.scala @@ -19,7 +19,7 @@ package org.apache.spark.ui import java.{util => ju} import java.lang.{Long => JLong} -import java.net.{URLDecoder, URLEncoder} +import java.net.{URI, URISyntaxException, URLDecoder, URLEncoder} import java.nio.charset.StandardCharsets.UTF_8 import java.time.{Instant, ZoneId} import java.time.format.DateTimeFormatter @@ -787,8 +787,22 @@ private[spark] object UIUtils extends Logging { if (proxy) { val proxyPrefix = sys.props.getOrElse("spark.ui.proxyBase", "") proxyPrefix + "/proxy/" + id - } else { + } else if (isSafeHref(origHref)) { origHref + } else { + // The href may come from an external registrant (e.g. an application's appUiUrl or + // a worker's webUiAddress); render anything unrecognized as a dead link. + "#" + } + } + + /** Returns true if the given href is relative or uses the http(s) scheme. */ + private def isSafeHref(href: String): Boolean = href != null && { + try { + val scheme = new URI(href).getScheme + scheme == null || scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https") + } catch { + case _: URISyntaxException => false } } diff --git a/core/src/test/scala/org/apache/spark/ui/UIUtilsSuite.scala b/core/src/test/scala/org/apache/spark/ui/UIUtilsSuite.scala index 3670b60e07292..a3ac4bac125d6 100644 --- a/core/src/test/scala/org/apache/spark/ui/UIUtilsSuite.scala +++ b/core/src/test/scala/org/apache/spark/ui/UIUtilsSuite.scala @@ -244,4 +244,14 @@ class UIUtilsSuite extends SparkFunSuite { assert(html.contains("data-toggle-details"), "detailsUINode should use data-toggle-details") assert(html.contains("stacktrace-details"), "detailsUINode should contain stacktrace-details") } + + test("makeHref only renders http(s) or relative URLs as hyperlinks") { + // appUiUrl and webUiAddress come from external registrants. + assert(UIUtils.makeHref(proxy = false, "app-1", "http://host:4040") === "http://host:4040") + assert(UIUtils.makeHref(proxy = false, "app-1", "https://host:4040") === "https://host:4040") + assert(UIUtils.makeHref(proxy = false, "app-1", "/relative/path") === "/relative/path") + assert(UIUtils.makeHref(proxy = false, "app-1", null) === "#") + // Reverse-proxy mode is unaffected: the href is generated by the master itself. + assert(UIUtils.makeHref(proxy = true, "app-1", "badlink").endsWith("/proxy/app-1")) + } }