This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch branch-3.5
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/branch-3.5 by this push:
new c85adcbdb037 [BACKPORT][MINOR][UI] Encode query parameters when
generating log page scripts
c85adcbdb037 is described below
commit c85adcbdb037a7ad46fa28c4b9a1177b30d0afe6
Author: Holden Karau <[email protected]>
AuthorDate: Fri Jul 10 22:14:46 2026 -0700
[BACKPORT][MINOR][UI] Encode query parameters when generating log page
scripts
### What changes were proposed in this pull request?
The log viewer pages assemble a query string from request parameters and
embed it into an inline script literal. Encode that value for a JavaScript
string context using the bundled commons-text StringEscapeUtils, matching the
existing pattern in AllJobsPage, so the generated script stays well-formed
regardless of the parameter contents.
Backport to 3.5 of https://github.com/apache/spark/pull/57165
### Why are the changes needed?
Formatting can get funky with unescaped parameters.
### Does this PR introduce _any_ user-facing change?
No
### How was this patch tested?
New test
### Was this patch authored or co-authored using generative AI tooling?
Primarily Claude 4.8
Closes #57196 from holdenk/log-page-encode-3.5.
Lead-authored-by: Holden Karau <[email protected]>
Co-authored-by: Claude <Holden's Claude Robot [email protected]>
Co-authored-by: Holden Karau <[email protected]>
Signed-off-by: Holden Karau <[email protected]>
---
.../apache/spark/deploy/worker/ui/LogPage.scala | 27 +++++++++++++++++----
.../spark/deploy/worker/ui/LogPageSuite.scala | 28 ++++++++++++++++++++++
2 files changed, 51 insertions(+), 4 deletions(-)
diff --git
a/core/src/main/scala/org/apache/spark/deploy/worker/ui/LogPage.scala
b/core/src/main/scala/org/apache/spark/deploy/worker/ui/LogPage.scala
index 6800a19b549b..7b80527ff338 100644
--- a/core/src/main/scala/org/apache/spark/deploy/worker/ui/LogPage.scala
+++ b/core/src/main/scala/org/apache/spark/deploy/worker/ui/LogPage.scala
@@ -18,10 +18,14 @@
package org.apache.spark.deploy.worker.ui
import java.io.File
+import java.net.URLEncoder
+import java.nio.charset.StandardCharsets.UTF_8
import javax.servlet.http.HttpServletRequest
import scala.xml.{Node, Unparsed}
+import org.apache.commons.text.StringEscapeUtils
+
import org.apache.spark.internal.Logging
import org.apache.spark.ui.{UIUtils, WebUIPage}
import org.apache.spark.util.Utils
@@ -33,6 +37,12 @@ private[ui] class LogPage(parent: WorkerWebUI) extends
WebUIPage("logPage") with
private val supportedLogTypes = Set("stderr", "stdout")
private val defaultBytes = 100 * 1024
+ // URL-encode a single request-supplied value before embedding it in a log
page query string, so
+ // characters such as '&', '=', '#' and whitespace cannot change the
structure of the subsequent
+ // /log requests (parameter injection / query truncation).
+ private def encodeParam(value: String): String =
+ URLEncoder.encode(Option(value).getOrElse("null"), UTF_8.name())
+
def renderLog(request: HttpServletRequest): String = {
val appId = Option(request.getParameter("appId"))
val executorId = Option(request.getParameter("executorId"))
@@ -65,11 +75,16 @@ private[ui] class LogPage(parent: WorkerWebUI) extends
WebUIPage("logPage") with
val byteLength = Option(request.getParameter("byteLength")).map(_.toInt)
.getOrElse(defaultBytes)
+ // The identifiers below flow both into a filesystem path (logDir, kept
raw and guarded by
+ // Utils.isInDirectory in getLog) and into the query string of the
generated /log requests
+ // (params). URL-encode the query-string copy so request-supplied values
cannot inject extra
+ // parameters or truncate the request.
val (logDir, params, pageName) = (appId, executorId, driverId) match {
case (Some(a), Some(e), None) =>
- (s"${workDir.getPath}/$a/$e/", s"appId=$a&executorId=$e", s"$a/$e")
+ (s"${workDir.getPath}/$a/$e/",
+ s"appId=${encodeParam(a)}&executorId=${encodeParam(e)}", s"$a/$e")
case (None, None, Some(d)) =>
- (s"${workDir.getPath}/$d/", s"driverId=$d", d)
+ (s"${workDir.getPath}/$d/", s"driverId=${encodeParam(d)}", d)
case _ =>
throw new Exception("Request must specify either application or driver
identifiers")
}
@@ -97,9 +112,13 @@ private[ui] class LogPage(parent: WorkerWebUI) extends
WebUIPage("logPage") with
End of Log
</div>
- val logParams = "?%s&logType=%s".format(params, logType)
+ val logParams = "?%s&logType=%s".format(params, encodeParam(logType))
+ // logParams carries request-supplied values into an inline script
literal, so escape it for a
+ // JavaScript string context before embedding it via Unparsed below
(defense in depth on top of
+ // the URL-encoding above).
val jsOnload = "window.onload = " +
- s"initLogPage('$logParams', $curLogLength, $startByte, $endByte,
$logLength, $byteLength);"
+ s"initLogPage('${StringEscapeUtils.escapeEcmaScript(logParams)}', " +
+ s"$curLogLength, $startByte, $endByte, $logLength, $byteLength);"
val content =
<script src={UIUtils.prependBaseUri(request,
"/static/utils.js")}></script> ++
diff --git
a/core/src/test/scala/org/apache/spark/deploy/worker/ui/LogPageSuite.scala
b/core/src/test/scala/org/apache/spark/deploy/worker/ui/LogPageSuite.scala
index c8b4e3372386..7030b9f5bed5 100644
--- a/core/src/test/scala/org/apache/spark/deploy/worker/ui/LogPageSuite.scala
+++ b/core/src/test/scala/org/apache/spark/deploy/worker/ui/LogPageSuite.scala
@@ -18,6 +18,7 @@
package org.apache.spark.deploy.worker.ui
import java.io.{File, FileWriter}
+import javax.servlet.http.HttpServletRequest
import org.mockito.Mockito.{mock, when}
import org.scalatest.PrivateMethodTester
@@ -75,6 +76,33 @@ class LogPageSuite extends SparkFunSuite with
PrivateMethodTester {
assert(error4.startsWith("Error: invalid log directory"))
}
+ test("render encodes request parameters embedded in the inline script") {
+ val webui = mock(classOf[WorkerWebUI])
+ val worker = mock(classOf[Worker])
+ withTempDir { tmpDir =>
+ val workDir = new File(tmpDir, "work-dir")
+ workDir.mkdir()
+ when(webui.workDir).thenReturn(workDir)
+ when(webui.worker).thenReturn(worker)
+ when(worker.conf).thenReturn(new SparkConf())
+ when(worker.activeMasterWebUiUrl).thenReturn("http://master:8080")
+ val logPage = new LogPage(webui)
+
+ val request = mock(classOf[HttpServletRequest])
+ // appId is user-controlled; a '&' in it would inject an extra parameter
into the /log
+ // requests, and the '</script>' would break out of the inline <script>
block, if emitted raw.
+
when(request.getParameter("appId")).thenReturn("app&byteLength=0</script>")
+ when(request.getParameter("executorId")).thenReturn("0")
+ when(request.getParameter("logType")).thenReturn("stdout")
+ val html = logPage.render(request).mkString
+
+ // The value is percent-encoded, so it stays a single opaque appId value
and cannot inject a
+ // parameter or close the script element.
+
assert(html.contains("appId=app%26byteLength%3D0%3C%2Fscript%3E&executorId=0"))
+ assert(!html.contains("app&byteLength=0</script>"))
+ }
+ }
+
/** Write the specified string to the file. */
private def write(f: File, s: String): Unit = {
val writer = new FileWriter(f)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]