CVE-2023-37460 (published 2023-07-25T20:15Z)

Plexis Archiver is a collection of Plexus components to create archives or extract archives to a directory with a unified `Archiver`/`UnArchiver` API. Prior to version 4.8.0, using AbstractUnArchiver for extracting an archive might lead to an arbitrary file creation and possibly remote code execution. When extracting an archive with an entry that already exists in the destination directory as a symbolic link whose target does not exist - the `resolveFile()` function will return the symlink's source instead of its target, which will pass the verification that ensures the file will not be extracted outside of the destination directory. Later `Files.newOutputStream()`, that follows symlinks by default,  will actually write the entry's content to the symlink's target. Whoever uses plexus archiver to extract an untrusted archive is vulnerable to an arbitrary file creation and possibly remote code execution. Version 4.8.0 contains a patch for this issue.

CVSS v3 base score: 9.8, CRITICAL (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-22
cwe_name: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
extended_description: 
            Many file operations are intended to take place within a restricted directory. By using special elements such as ".." and "/" separators, attackers can escape outside of the restricted location to access files or directories that are elsewhere on the system. One of the most common special elements is the "../" sequence, which in most modern operating systems is interpreted as the parent directory of the current location. This is referred to as relative path traversal. Path traversal also covers the use of absolute pathnames such as "/usr/local/bin" to access unexpected files. This is referred to as absolute path traversal.
         
         
cwe_id: CWE-61
cwe_name: UNIX Symbolic Link (Symlink) Following
description: The product, when opening a file or directory, does not sufficiently account for when the file is a symbolic link that resolves to a target outside of the intended control sphere. This could allow an attacker to cause the product to operate on unauthorized files.
extended_description: A product that allows UNIX symbolic links (symlink) as part of paths whether in internal code or through user input can allow an attacker to spoof the symbolic link and traverse the file system to unintended locations or access arbitrary files. The symbolic link can permit an attacker to read/write/corrupt a file that they originally did not have permissions to access.
         

Fix information:
repo_url: https://github.com/codehaus-plexus/plexus-archiver
commit_details: repo_url: https://github.com/codehaus-plexus/plexus-archiver
author: uriyay-jfrog
author_date: 2023-07-10 10:59:52+03:00
msg: Avoid override target symlink by standard file in AbstractUnArchiver
repository: repo_url: https://github.com/codehaus-plexus/plexus-archiver
repo_name: codehaus-plexus/plexus-archiver
repo_language: Java
owner: codehaus-plexus
file_changes: filename: AbstractUnArchiver.java
change_type: ModificationType.MODIFY
diff: @@ -20,7 +20,6 @@
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.OutputStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
@@ -35,11 +34,12 @@
 import org.codehaus.plexus.components.io.fileselectors.FileSelector;
 import org.codehaus.plexus.components.io.resources.PlexusIoResource;
 import org.codehaus.plexus.util.FileUtils;
-import org.codehaus.plexus.util.IOUtil;
 import org.codehaus.plexus.util.StringUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
+
 // TODO there should really be constructors which take the source file.
 
 /**
@@ -301,6 +301,11 @@ protected void extractFile(
             throw new ArchiverException("Entry is outside of the target directory (" + entryName + ")");
         }
 
+        // don't allow override target symlink by standard file
+        if (StringUtils.isEmpty(symlinkDestination) && Files.isSymbolicLink(canonicalDestPath)) {
+            throw new ArchiverException("Entry is outside of the target directory (" + entryName + ")");
+        }
+
         try {
             if (!shouldExtractEntry(dir, targetFileName, entryName, entryDate)) {
                 return;
@@ -317,9 +322,7 @@ protected void extractFile(
             } else if (isDirectory) {
                 targetFileName.mkdirs();
             } else {
-                try (OutputStream out = Files.newOutputStream(targetFileName.toPath())) {
-                    IOUtil.copy(compressedInputStream, out);
-                }
+                Files.copy(compressedInputStream, targetFileName.toPath(), REPLACE_EXISTING);
             }
 
             targetFileName.setLastModified(entryDate.getTime());

programming_language: Java
filename: SymlinkTest.java
change_type: ModificationType.MODIFY
diff: @@ -62,6 +62,8 @@ void testSymlinkTar() throws Exception {
         unarchiver.setSourceFile(archiveFile);
         unarchiver.setDestFile(output);
         unarchiver.extract();
+        // second unpacking should also work
+        unarchiver.extract();
     }
 
     @Test
@@ -81,6 +83,8 @@ void testSymlinkZip() throws Exception {
         unarchiver.setSourceFile(archiveFile);
         unarchiver.setDestFile(output);
         unarchiver.extract();
+        // second unpacking should also work
+        unarchiver.extract();
     }
 
     @Test

programming_language: Java
filename: ZipArchiverTest.java
change_type: ModificationType.MODIFY
diff: @@ -85,6 +85,7 @@
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 
@@ -809,6 +810,17 @@ void testFixedEntryModificationTime() throws IOException {
         }
     }
 
+    @Test
+    @DisabledOnOs(OS.WINDOWS)
+    void testNonExistingSymlink() throws Exception {
+        File zipFile = new File("src/test/resources/symlinks/non_existing_symlink.zip");
+        ZipUnArchiver unArchiver = getZipUnArchiver(zipFile);
+        String tmpdir = Files.createTempDirectory("tmpe_extract").toFile().getAbsolutePath();
+        unArchiver.setDestDirectory(new File(tmpdir));
+        ArchiverException exception = assertThrows(ArchiverException.class, unArchiver::extract);
+        assertEquals("Entry is outside of the target directory (entry1)", exception.getMessage());
+    }
+
     /**
      * Takes a timestamp, turns it into a textual representation based on GMT, then translated it into a timestamp in
      * local timezone. This makes the test independent of the current TimeZone. The reason this is necessary is:

programming_language: Java
filename: regen.sh
change_type: ModificationType.MODIFY
diff: @@ -4,3 +4,13 @@ cd src
 zip --symlinks ../symlinks.zip file* targetDir sym*
 tar -cvf ../symlinks.tar file* targetDir sym*
 
+cd ..
+rm non_existing_symlink.zip
+mkdir non_existing_symlink
+cd non_existing_symlink
+ln -s /tmp/target entry1
+echo -ne 'content' > entry2
+zip  --symlinks ../non_existing_symlink.zip entry1 entry2
+cd ..
+rm -rf non_existing_symlink
+LC_ALL=C sed  -i '' 's/entry2/entry1/' non_existing_symlink.zip
\ No newline at end of file

programming_language: Shell