Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions src/main/java/org/codehaus/plexus/archiver/AbstractUnArchiver.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
import org.codehaus.plexus.components.io.filemappers.FileMapper;
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.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -290,21 +289,32 @@ protected void extractFile(
}
}

// Hmm. Symlinks re-evaluate back to the original file here. Unsure if this is a good thing...
final File targetFileName = FileUtils.resolveFile(dir, entryName);
// Don't use FileUtils.resolveFile as it follows symlinks, which would cause issues
// when trying to overwrite an existing symlink
final File targetFileName = new File(dir, entryName);

// Make sure that the resolved path of the extracted file doesn't escape the destination directory
// getCanonicalFile().toPath() is used instead of getCanonicalPath() (returns String),
// because "/opt/directory".startsWith("/opt/dir") would return false negative.
Path canonicalDirPath = dir.getCanonicalFile().toPath();
Path canonicalDestPath = targetFileName.getCanonicalFile().toPath();
// Don't follow symlinks for the target file, to avoid issues with symlink handling
Path targetPath = targetFileName.toPath();
// For security check, we need the canonical path but without following the last symlink
Path canonicalDestPath;
if (Files.isSymbolicLink(targetPath)) {
// If it's a symlink, get the canonical path of the parent and append the file name
canonicalDestPath =
targetFileName.getParentFile().getCanonicalFile().toPath().resolve(targetFileName.getName());
} else {
canonicalDestPath = targetFileName.getCanonicalFile().toPath();
}

if (!canonicalDestPath.startsWith(canonicalDirPath)) {
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)) {
if (StringUtils.isEmpty(symlinkDestination) && Files.isSymbolicLink(targetPath)) {
throw new ArchiverException("Entry is outside of the target directory (" + entryName + ")");
}

Expand All @@ -320,6 +330,11 @@ protected void extractFile(
}

if (!StringUtils.isEmpty(symlinkDestination)) {
// Delete existing symlink if it exists
Path symlinkPath = targetFileName.toPath();
if (Files.isSymbolicLink(symlinkPath)) {
Files.delete(symlinkPath);
}
SymlinkUtils.createSymbolicLink(targetFileName, new File(symlinkDestination));
} else if (isDirectory) {
targetFileName.mkdirs();
Expand Down Expand Up @@ -362,6 +377,11 @@ protected boolean shouldExtractEntry(File targetDirectory, File targetFileName,
// scenario (4) and (5).
// No matter the case sensitivity of the file system, file.exists() returns false when there is no file with the
// same name (1).
// Check if it's a symlink first, as exists() follows symlinks and may give incorrect results
if (Files.isSymbolicLink(targetFileName.toPath())) {
// For symlinks, always extract to overwrite the existing symlink
return true;
}
if (!targetFileName.exists()) {
return true;
}
Expand Down
153 changes: 153 additions & 0 deletions src/test/java/org/codehaus/plexus/archiver/SymlinkTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.condition.OS;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

Expand Down Expand Up @@ -107,4 +108,156 @@ void testSymlinkDirArchiver() throws Exception {
symbolicLink = new File("target/output/dirarchiver-symlink/aDirWithALink/backOutsideToFileX");
assertTrue(Files.isSymbolicLink(symbolicLink.toPath()));
}

@Test
@DisabledOnOs(OS.WINDOWS)
void testSymlinkOverwriteZip() throws Exception {
// Create temporary directory structure for testing
File tempDir = new File("target/test-symlink-overwrite");
// Clean up from any previous test runs
if (tempDir.exists()) {
org.codehaus.plexus.util.FileUtils.deleteDirectory(tempDir);
}
tempDir.mkdirs();

// Create two target files
File target1 = new File(tempDir, "target1.txt");
File target2 = new File(tempDir, "target2.txt");
Files.write(target1.toPath(), "content1".getBytes());
Files.write(target2.toPath(), "content2".getBytes());

// Create first archive with symlink pointing to target1
File archive1Dir = new File(tempDir, "archive1");
archive1Dir.mkdirs();
File archive1Target1 = new File(archive1Dir, "target1.txt");
Files.write(archive1Target1.toPath(), "content1".getBytes());
Files.createSymbolicLink(
new File(archive1Dir, "link.txt").toPath(),
archive1Target1.toPath().getFileName());

ZipArchiver archiver1 = (ZipArchiver) lookup(Archiver.class, "zip");
archiver1.addDirectory(archive1Dir);
File zipFile1 = new File(tempDir, "archive1.zip");
archiver1.setDestFile(zipFile1);
archiver1.createArchive();

// Extract first archive
File outputDir = new File(tempDir, "output");
outputDir.mkdirs();
ZipUnArchiver unarchiver1 = (ZipUnArchiver) lookup(UnArchiver.class, "zip");
unarchiver1.setSourceFile(zipFile1);
unarchiver1.setDestFile(outputDir);
unarchiver1.extract();

// Verify symlink points to target1.txt
File extractedLink = new File(outputDir, "link.txt");
assertTrue(Files.isSymbolicLink(extractedLink.toPath()));
assertEquals(
"target1.txt", Files.readSymbolicLink(extractedLink.toPath()).toString());

// Create second archive with symlink pointing to target2
File archive2Dir = new File(tempDir, "archive2");
archive2Dir.mkdirs();
File archive2Target2 = new File(archive2Dir, "target2.txt");
Files.write(archive2Target2.toPath(), "content2".getBytes());
Files.createSymbolicLink(
new File(archive2Dir, "link.txt").toPath(),
archive2Target2.toPath().getFileName());

ZipArchiver archiver2 = (ZipArchiver) lookup(Archiver.class, "zip");
archiver2.addDirectory(archive2Dir);
File zipFile2 = new File(tempDir, "archive2.zip");
archiver2.setDestFile(zipFile2);
archiver2.createArchive();

// Extract second archive (should overwrite the symlink)
ZipUnArchiver unarchiver2 = (ZipUnArchiver) lookup(UnArchiver.class, "zip");
unarchiver2.setSourceFile(zipFile2);
unarchiver2.setDestFile(outputDir);
unarchiver2.extract();

// Verify symlink now points to target2.txt (THIS IS THE KEY TEST)
assertTrue(Files.isSymbolicLink(extractedLink.toPath()), "link.txt should still be a symlink");
assertEquals(
"target2.txt",
Files.readSymbolicLink(extractedLink.toPath()).toString(),
"Symlink should be updated to point to target2.txt");
}

@Test
@DisabledOnOs(OS.WINDOWS)
void testSymlinkOverwriteTar() throws Exception {
// Create temporary directory structure for testing
File tempDir = new File("target/test-symlink-overwrite-tar");
// Clean up from any previous test runs
if (tempDir.exists()) {
org.codehaus.plexus.util.FileUtils.deleteDirectory(tempDir);
}
tempDir.mkdirs();

// Create two target files
File target1 = new File(tempDir, "target1.txt");
File target2 = new File(tempDir, "target2.txt");
Files.write(target1.toPath(), "content1".getBytes());
Files.write(target2.toPath(), "content2".getBytes());

// Create first archive with symlink pointing to target1
File archive1Dir = new File(tempDir, "archive1");
archive1Dir.mkdirs();
File archive1Target1 = new File(archive1Dir, "target1.txt");
Files.write(archive1Target1.toPath(), "content1".getBytes());
Files.createSymbolicLink(
new File(archive1Dir, "link.txt").toPath(),
archive1Target1.toPath().getFileName());

TarArchiver archiver1 = (TarArchiver) lookup(Archiver.class, "tar");
archiver1.setLongfile(TarLongFileMode.posix);
archiver1.addDirectory(archive1Dir);
File tarFile1 = new File(tempDir, "archive1.tar");
archiver1.setDestFile(tarFile1);
archiver1.createArchive();

// Extract first archive
File outputDir = new File(tempDir, "output");
outputDir.mkdirs();
TarUnArchiver unarchiver1 = (TarUnArchiver) lookup(UnArchiver.class, "tar");
unarchiver1.setSourceFile(tarFile1);
unarchiver1.setDestFile(outputDir);
unarchiver1.extract();

// Verify symlink points to target1.txt
File extractedLink = new File(outputDir, "link.txt");
assertTrue(Files.isSymbolicLink(extractedLink.toPath()));
assertEquals(
"target1.txt", Files.readSymbolicLink(extractedLink.toPath()).toString());

// Create second archive with symlink pointing to target2
File archive2Dir = new File(tempDir, "archive2");
archive2Dir.mkdirs();
File archive2Target2 = new File(archive2Dir, "target2.txt");
Files.write(archive2Target2.toPath(), "content2".getBytes());
Files.createSymbolicLink(
new File(archive2Dir, "link.txt").toPath(),
archive2Target2.toPath().getFileName());

TarArchiver archiver2 = (TarArchiver) lookup(Archiver.class, "tar");
archiver2.setLongfile(TarLongFileMode.posix);
archiver2.addDirectory(archive2Dir);
File tarFile2 = new File(tempDir, "archive2.tar");
archiver2.setDestFile(tarFile2);
archiver2.createArchive();

// Extract second archive (should overwrite the symlink)
TarUnArchiver unarchiver2 = (TarUnArchiver) lookup(UnArchiver.class, "tar");
unarchiver2.setSourceFile(tarFile2);
unarchiver2.setDestFile(outputDir);
unarchiver2.extract();

// Verify symlink now points to target2.txt (THIS IS THE KEY TEST)
assertTrue(Files.isSymbolicLink(extractedLink.toPath()), "link.txt should still be a symlink");
assertEquals(
"target2.txt",
Files.readSymbolicLink(extractedLink.toPath()).toString(),
"Symlink should be updated to point to target2.txt");
}
}
Loading