This is an automated email from the ASF dual-hosted git repository.

dongjoon-hyun pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.x by this push:
     new 217f61536a88 [SPARK-58153][CORE][R] Reject JAR entries escaping the 
extraction directory in `RPackageUtils`
217f61536a88 is described below

commit 217f61536a88aef671d1184622b12441b514851e
Author: Dongjoon Hyun <[email protected]>
AuthorDate: Wed Jul 15 14:30:46 2026 -0700

    [SPARK-58153][CORE][R] Reject JAR entries escaping the extraction directory 
in `RPackageUtils`
    
    ### What changes were proposed in this pull request?
    
    This PR fixes a zip-slip (arbitrary file write) vulnerability in 
`RPackageUtils.extractRFolder`, which derived its output path from a JAR entry 
name without any containment check. An entry named `R/pkg/../../../../.bashrc` 
satisfies `indexOf("R/pkg") == 0`, so the `../` sequences survived into the 
path and `new File(tempDir, entryPath)` resolved outside `tempDir`.
    
    The output path is now computed once, above the directory/file branch, and 
validated before either branch uses it:
    
    ```scala
    val outPath = new File(tempDir, entryPath).getCanonicalFile
    if (!outPath.getCanonicalPath.startsWith(tempDir.getCanonicalPath + 
File.separator)) {
      throw new IOException(s"Malicious zip entry escaping target dir: 
${entry.getName}")
    }
    ```
    
    ### Why are the changes needed?
    
    `extractRFolder` runs from `SparkSubmit` (via `checkAndBuildRPackage`) for 
any `--jars` / `--packages` JAR whose manifest carries the `Spark-HasRPackage` 
flag. Submitting a malicious JAR could write arbitrary files outside `tempDir` 
on the submitting host — overwriting `~/.bashrc`, for example, escalates to 
code execution.
    
    ### Does this PR introduce _any_ user-facing change?
    
    - No. Well-formed R package JARs extract as before; only entries resolving 
outside the extraction directory are now rejected.
    - Note that `SparkR` is deprecated.
    
    ### How was this patch tested?
    
    Pass the CIs with the newly added test case.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Fable 5
    
    Closes #57283 from dongjoon-hyun/SPARK-58153.
    
    Authored-by: Dongjoon Hyun <[email protected]>
    Signed-off-by: Dongjoon Hyun <[email protected]>
    (cherry picked from commit e9121bbf5a94b724b195a6f11c35a1a2d562e3d5)
    Signed-off-by: Dongjoon Hyun <[email protected]>
---
 .../org/apache/spark/deploy/RPackageUtils.scala    | 10 +++++---
 .../apache/spark/deploy/RPackageUtilsSuite.scala   | 29 +++++++++++++++++++---
 2 files changed, 32 insertions(+), 7 deletions(-)

diff --git a/core/src/main/scala/org/apache/spark/deploy/RPackageUtils.scala 
b/core/src/main/scala/org/apache/spark/deploy/RPackageUtils.scala
index f14c2dd69397..29cddb76209f 100644
--- a/core/src/main/scala/org/apache/spark/deploy/RPackageUtils.scala
+++ b/core/src/main/scala/org/apache/spark/deploy/RPackageUtils.scala
@@ -146,15 +146,17 @@ private[deploy] object RPackageUtils extends Logging {
       val entryRIndex = entry.getName.indexOf(RJarEntries)
       if (entryRIndex > -1) {
         val entryPath = entry.getName.substring(entryRIndex)
+        val outPath = new File(tempDir, entryPath).getCanonicalFile
+        if (!outPath.getCanonicalPath.startsWith(tempDir.getCanonicalPath + 
File.separator)) {
+          throw new IOException(s"Malicious zip entry escaping target dir: 
${entry.getName}")
+        }
         if (entry.isDirectory) {
-          val dir = new File(tempDir, entryPath)
           if (verbose) {
-            print(log"Creating directory: ${MDC(PATH, dir)}", printStream)
+            print(log"Creating directory: ${MDC(PATH, outPath)}", printStream)
           }
-          Utils.createDirectory(dir)
+          Utils.createDirectory(outPath)
         } else {
           val inStream = jar.getInputStream(entry)
-          val outPath = new File(tempDir, entryPath)
           Utils.createParentDirs(outPath)
           val outStream = new FileOutputStream(outPath)
           if (verbose) {
diff --git 
a/core/src/test/scala/org/apache/spark/deploy/RPackageUtilsSuite.scala 
b/core/src/test/scala/org/apache/spark/deploy/RPackageUtilsSuite.scala
index 84e6b6971c27..da7b4bd960e3 100644
--- a/core/src/test/scala/org/apache/spark/deploy/RPackageUtilsSuite.scala
+++ b/core/src/test/scala/org/apache/spark/deploy/RPackageUtilsSuite.scala
@@ -17,11 +17,12 @@
 
 package org.apache.spark.deploy
 
-import java.io.{File, OutputStream, PrintStream}
+import java.io.{File, FileOutputStream, IOException, OutputStream, PrintStream}
 import java.net.URI
-import java.util.jar.{JarFile, Manifest}
+import java.nio.charset.StandardCharsets
+import java.util.jar.{JarFile, JarOutputStream, Manifest}
 import java.util.jar.Attributes.Name
-import java.util.zip.ZipFile
+import java.util.zip.{ZipEntry, ZipFile}
 
 import scala.collection.mutable.ArrayBuffer
 import scala.jdk.CollectionConverters._
@@ -138,6 +139,28 @@ class RPackageUtilsSuite
     }
   }
 
+  test("SPARK-58153: jar entries escaping the extraction directory are 
rejected") {
+    val tempDir = Utils.createTempDir()
+    Utils.tryWithSafeFinally {
+      val manifest = new Manifest
+      val attr = manifest.getMainAttributes
+      attr.put(Name.MANIFEST_VERSION, "1.0")
+      attr.put(new Name("Spark-HasRPackage"), "true")
+      val jar = new File(tempDir, "malicious.jar")
+      Utils.tryWithResource(new JarOutputStream(new FileOutputStream(jar), 
manifest)) { out =>
+        out.putNextEntry(new ZipEntry("R/pkg/../../../../evil.txt"))
+        out.write("evil".getBytes(StandardCharsets.UTF_8))
+        out.closeEntry()
+      }
+      val e = intercept[IOException] {
+        RPackageUtils.checkAndBuildRPackage(jar.getAbsolutePath, new 
BufferPrintStream)
+      }
+      assert(e.getMessage.contains("Malicious zip entry"))
+    } {
+      Utils.deleteRecursively(tempDir)
+    }
+  }
+
   test("SparkR zipping works properly") {
     val tempDir = Utils.createTempDir()
     Utils.tryWithSafeFinally {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to