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]

Reply via email to