CVE-2023-37460 (Java, published 2023-07-25T20:15Z)
Description: value: 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.
Severity: nan (CVSS v3 base score: 9.8)
Weakness: CWE-22 \u2014 Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
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.
Repository: https://github.com/codehaus-plexus/plexus-archiver
Files changed:
src/main/java/org/codehaus/plexus/archiver/AbstractUnArchiver.java
src/test/java/org/codehaus/plexus/archiver/SymlinkTest.java
src/test/java/org/codehaus/plexus/archiver/zip/ZipArchiverTest.java
src/test/resources/symlinks/regen.sh
Commit message: Avoid override target symlink by standard file in AbstractUnArchiver


Vulnerable code:
import java.io.OutputStream;
import org.codehaus.plexus.util.IOUtil;
                try (OutputStream out = Files.newOutputStream(targetFileName.toPath())) {
                    IOUtil.copy(compressedInputStream, out);
                }

Fixed code:
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;

        // 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 + ")");
        }

                Files.copy(compressedInputStream, targetFileName.toPath(), REPLACE_EXISTING);
        // second unpacking should also work
        unarchiver.extract();
        // second unpacking should also work
        unarchiver.extract();
import static org.junit.jupiter.api.Assertions.assertThrows;
    @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());
    }

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