File plexus-utils-CVE-2025-67030.patch of Package plexus-utils

From 6d780b3378829318ba5c2d29547e0012d5b29642 Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Sun, 9 Nov 2025 12:32:38 +0100
Subject: [PATCH] Fix Zip Slip vulnerability in archive extraction (#296)

---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: slachiewicz <6705942+slachiewicz@users.noreply.github.com>
---
 .../java/org/codehaus/plexus/util/Expand.java |  15 +-
 .../org/codehaus/plexus/util/ExpandTest.java  | 148 ++++++++++++++++++
 2 files changed, 161 insertions(+), 2 deletions(-)
 create mode 100644 src/test/java/org/codehaus/plexus/util/ExpandTest.java

diff --git a/src/main/java/org/codehaus/plexus/util/Expand.java b/src/main/java/org/codehaus/plexus/util/Expand.java
index 8abcdd95..6ac71dc1 100644
--- a/src/main/java/org/codehaus/plexus/util/Expand.java
+++ b/src/main/java/org/codehaus/plexus/util/Expand.java
@@ -111,8 +111,19 @@ protected void extractFile(
             throws Exception {
         File f = FileUtils.resolveFile(dir, entryName);
 
-        if (!f.getAbsolutePath().startsWith(dir.getAbsolutePath())) {
-            throw new IOException("Entry '" + entryName + "' outside the target directory.");
+        try {
+            String canonicalDirPath = dir.getCanonicalPath();
+            String canonicalFilePath = f.getCanonicalPath();
+
+            // Ensure the file is within the target directory
+            // We need to check that the canonical file path starts with the canonical directory path
+            // followed by a file separator to prevent path traversal attacks
+            if (!canonicalFilePath.startsWith(canonicalDirPath + File.separator)
+                    && !canonicalFilePath.equals(canonicalDirPath)) {
+                throw new IOException("Entry '" + entryName + "' outside the target directory.");
+            }
+        } catch (IOException e) {
+            throw new IOException("Failed to verify entry path for '" + entryName + "'", e);
         }
 
         try {
diff --git a/src/test/java/org/codehaus/plexus/util/ExpandTest.java b/src/test/java/org/codehaus/plexus/util/ExpandTest.java
new file mode 100644
index 00000000..46e3d0e3
--- /dev/null
+++ b/src/test/java/org/codehaus/plexus/util/ExpandTest.java
@@ -0,0 +1,148 @@
+package org.codehaus.plexus.util;
+
+/*
+ * Copyright The Codehaus Foundation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.io.File;
+import java.nio.file.Files;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Test for {@link Expand}.
+ */
+class ExpandTest extends FileBasedTestCase {
+
+    @Test
+    void testZipSlipVulnerabilityWithParentDirectory() throws Exception {
+        File tempDir = getTestDirectory();
+        File zipFile = new File(tempDir, "malicious.zip");
+        File targetDir = new File(tempDir, "extract");
+        targetDir.mkdirs();
+
+        // Create a malicious zip with path traversal
+        try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(zipFile.toPath()))) {
+            ZipEntry entry = new ZipEntry("../../evil.txt");
+            zos.putNextEntry(entry);
+            zos.write("malicious content".getBytes());
+            zos.closeEntry();
+        }
+
+        Expand expand = new Expand();
+        expand.setSrc(zipFile);
+        expand.setDest(targetDir);
+
+        // This should throw an exception, not extract the file
+        assertThrows(Exception.class, () -> expand.execute());
+
+        // Verify the file was not created outside the target directory
+        File evilFile = new File(tempDir, "evil.txt");
+        assertFalse(evilFile.exists(), "File should not be extracted outside target directory");
+    }
+
+    @Test
+    void testZipSlipVulnerabilityWithAbsolutePath() throws Exception {
+        File tempDir = getTestDirectory();
+        File zipFile = new File(tempDir, "malicious-absolute.zip");
+        File targetDir = new File(tempDir, "extract-abs");
+        targetDir.mkdirs();
+
+        // Create a malicious zip with absolute path
+        File evilTarget = new File("/tmp/evil-absolute.txt");
+        try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(zipFile.toPath()))) {
+            ZipEntry entry = new ZipEntry(evilTarget.getAbsolutePath());
+            zos.putNextEntry(entry);
+            zos.write("malicious content".getBytes());
+            zos.closeEntry();
+        }
+
+        Expand expand = new Expand();
+        expand.setSrc(zipFile);
+        expand.setDest(targetDir);
+
+        // This should throw an exception, not extract the file
+        assertThrows(Exception.class, () -> expand.execute());
+
+        // Verify the file was not created at the absolute path
+        assertFalse(evilTarget.exists(), "File should not be extracted to absolute path");
+    }
+
+    @Test
+    void testZipSlipVulnerabilityWithSimilarDirectoryName() throws Exception {
+        File tempDir = getTestDirectory();
+        File zipFile = new File(tempDir, "malicious-similar.zip");
+        File targetDir = new File(tempDir, "extract");
+        targetDir.mkdirs();
+
+        // Create a directory with a similar name to test prefix matching vulnerability
+        File similarDir = new File(tempDir, "extract-evil");
+        similarDir.mkdirs();
+
+        // Create a malicious zip that tries to exploit prefix matching
+        // If targetDir is /tmp/extract, this tries to write to /tmp/extract-evil/file.txt
+        String maliciousPath = "../extract-evil/evil.txt";
+        try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(zipFile.toPath()))) {
+            ZipEntry entry = new ZipEntry(maliciousPath);
+            zos.putNextEntry(entry);
+            zos.write("malicious content".getBytes());
+            zos.closeEntry();
+        }
+
+        Expand expand = new Expand();
+        expand.setSrc(zipFile);
+        expand.setDest(targetDir);
+
+        // This should throw an exception, not extract the file
+        assertThrows(Exception.class, () -> expand.execute());
+
+        // Verify the file was not created in the similar directory
+        File evilFile = new File(similarDir, "evil.txt");
+        assertFalse(evilFile.exists(), "File should not be extracted to directory with similar name");
+    }
+
+    @Test
+    void testNormalZipExtraction() throws Exception {
+        File tempDir = getTestDirectory();
+        File zipFile = new File(tempDir, "normal.zip");
+        File targetDir = new File(tempDir, "extract-normal");
+        targetDir.mkdirs();
+
+        // Create a normal zip
+        try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(zipFile.toPath()))) {
+            ZipEntry entry = new ZipEntry("subdir/normal.txt");
+            zos.putNextEntry(entry);
+            zos.write("normal content".getBytes());
+            zos.closeEntry();
+        }
+
+        Expand expand = new Expand();
+        expand.setSrc(zipFile);
+        expand.setDest(targetDir);
+
+        // This should succeed
+        expand.execute();
+
+        // Verify the file was created in the correct location
+        File normalFile = new File(targetDir, "subdir/normal.txt");
+        assertTrue(normalFile.exists(), "File should be extracted to correct location");
+    }
+}
openSUSE Build Service is sponsored by