Skip to content

Commit 39217a6

Browse files
Copilotslachiewicz
andcommitted
Fix symbolic link extraction issue by avoiding canonicalization for symlinks
Co-authored-by: slachiewicz <[email protected]>
1 parent a3a08fc commit 39217a6

File tree

2 files changed

+120
-3
lines changed

2 files changed

+120
-3
lines changed

src/main/java/org/codehaus/plexus/archiver/AbstractUnArchiver.java

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -290,14 +290,31 @@ protected void extractFile(
290290
}
291291
}
292292

293-
// Hmm. Symlinks re-evaluate back to the original file here. Unsure if this is a good thing...
294-
final File targetFileName = FileUtils.resolveFile(dir, entryName);
293+
// For symlinks, we need to get the file path without following symlinks.
294+
// FileUtils.resolveFile calls getCanonicalFile() which follows symlinks,
295+
// causing the symlink to resolve to its target instead of the symlink itself.
296+
final File targetFileName;
297+
if (!StringUtils.isEmpty(symlinkDestination)) {
298+
// For symlinks, use simple path resolution without canonicalization
299+
targetFileName = resolveFileWithoutFollowingSymlinks(dir, entryName);
300+
} else {
301+
// For regular files and directories, use the existing logic
302+
targetFileName = FileUtils.resolveFile(dir, entryName);
303+
}
295304

296305
// Make sure that the resolved path of the extracted file doesn't escape the destination directory
297306
// getCanonicalFile().toPath() is used instead of getCanonicalPath() (returns String),
298307
// because "/opt/directory".startsWith("/opt/dir") would return false negative.
299308
Path canonicalDirPath = dir.getCanonicalFile().toPath();
300-
Path canonicalDestPath = targetFileName.getCanonicalFile().toPath();
309+
310+
// For symlinks, we need to check the symlink path itself, not the target it points to
311+
Path canonicalDestPath;
312+
if (!StringUtils.isEmpty(symlinkDestination)) {
313+
// For symlinks, normalize without following the link
314+
canonicalDestPath = targetFileName.toPath().toAbsolutePath().normalize();
315+
} else {
316+
canonicalDestPath = targetFileName.getCanonicalFile().toPath();
317+
}
301318

302319
if (!canonicalDestPath.startsWith(canonicalDirPath)) {
303320
throw new ArchiverException("Entry is outside of the target directory (" + entryName + ")");
@@ -396,4 +413,33 @@ protected boolean shouldExtractEntry(File targetDirectory, File targetFileName,
396413
private String normalizedFileSeparator(String pathOrEntry) {
397414
return pathOrEntry.replace("/", File.separator);
398415
}
416+
417+
/**
418+
* Resolves a file path relative to a base directory without following symlinks.
419+
* This is similar to FileUtils.resolveFile but doesn't call getCanonicalFile(),
420+
* which would follow symlinks and resolve them to their targets.
421+
*
422+
* @param baseDir the base directory
423+
* @param filename the filename to resolve
424+
* @return the resolved file
425+
*/
426+
private File resolveFileWithoutFollowingSymlinks(File baseDir, String filename) {
427+
String filenm = filename;
428+
if ('/' != File.separatorChar) {
429+
filenm = filename.replace('/', File.separatorChar);
430+
}
431+
432+
if ('\\' != File.separatorChar) {
433+
filenm = filenm.replace('\\', File.separatorChar);
434+
}
435+
436+
// For absolute paths, just return a File object without canonicalization
437+
if (filenm.startsWith(File.separator)) {
438+
return new File(filenm);
439+
}
440+
441+
// For relative paths, combine with base directory and get absolute path
442+
// but don't call getCanonicalFile() which would follow symlinks
443+
return new File(baseDir, filenm).getAbsoluteFile();
444+
}
399445
}

src/test/java/org/codehaus/plexus/archiver/SymlinkTest.java

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,75 @@ void testSymlinkDirArchiver() throws Exception {
107107
symbolicLink = new File("target/output/dirarchiver-symlink/aDirWithALink/backOutsideToFileX");
108108
assertTrue(Files.isSymbolicLink(symbolicLink.toPath()));
109109
}
110+
111+
/**
112+
* Test for the issue where symlinks were not properly resolved when extracting archives.
113+
* When a symlink is extracted and then the archive is extracted again, the symlink
114+
* should remain a symlink and not be resolved to its target.
115+
* This test verifies:
116+
* 1. Extracting a symlink creates the symlink correctly
117+
* 2. Re-extracting the same archive preserves the symlink
118+
* 3. Symlinks to non-existent files are handled correctly
119+
*/
120+
@Test
121+
@DisabledOnOs(OS.WINDOWS)
122+
void testSymlinkExtractionTwice() throws Exception {
123+
// Test with tar
124+
TarArchiver tarArchiver = (TarArchiver) lookup(Archiver.class, "tar");
125+
tarArchiver.setLongfile(TarLongFileMode.posix);
126+
127+
File srcDir = getTestFile("src/test/resources/symlinks/src");
128+
tarArchiver.addDirectory(srcDir);
129+
File tarFile = new File("target/output/symlink-twice-test.tar");
130+
tarArchiver.setDestFile(tarFile);
131+
tarArchiver.createArchive();
132+
133+
File outputDir = new File("target/output/symlink-twice-test-tar");
134+
outputDir.mkdirs();
135+
136+
TarUnArchiver tarUnarchiver = (TarUnArchiver) lookup(UnArchiver.class, "tar");
137+
tarUnarchiver.setSourceFile(tarFile);
138+
tarUnarchiver.setDestFile(outputDir);
139+
140+
// First extraction
141+
tarUnarchiver.extract();
142+
143+
// Verify symlinks exist and are actually symlinks
144+
File symR = new File(outputDir, "symR");
145+
assertTrue(Files.isSymbolicLink(symR.toPath()), "symR should be a symlink");
146+
147+
// Second extraction - this should not fail and should preserve symlinks
148+
tarUnarchiver.extract();
149+
150+
// Verify symlinks still exist and are still symlinks
151+
assertTrue(Files.isSymbolicLink(symR.toPath()), "symR should still be a symlink after re-extraction");
152+
153+
// Test with zip
154+
ZipArchiver zipArchiver = (ZipArchiver) lookup(Archiver.class, "zip");
155+
zipArchiver.addDirectory(srcDir);
156+
File zipFile = new File("target/output/symlink-twice-test.zip");
157+
zipFile.delete();
158+
zipArchiver.setDestFile(zipFile);
159+
zipArchiver.createArchive();
160+
161+
File zipOutputDir = new File("target/output/symlink-twice-test-zip");
162+
zipOutputDir.mkdirs();
163+
164+
ZipUnArchiver zipUnarchiver = (ZipUnArchiver) lookup(UnArchiver.class, "zip");
165+
zipUnarchiver.setSourceFile(zipFile);
166+
zipUnarchiver.setDestFile(zipOutputDir);
167+
168+
// First extraction
169+
zipUnarchiver.extract();
170+
171+
// Verify symlinks exist and are actually symlinks
172+
File symRZip = new File(zipOutputDir, "symR");
173+
assertTrue(Files.isSymbolicLink(symRZip.toPath()), "symR should be a symlink in zip");
174+
175+
// Second extraction - this should not fail and should preserve symlinks
176+
zipUnarchiver.extract();
177+
178+
// Verify symlinks still exist and are still symlinks
179+
assertTrue(Files.isSymbolicLink(symRZip.toPath()), "symR should still be a symlink after re-extraction in zip");
180+
}
110181
}

0 commit comments

Comments
 (0)