From 8970cff5b0141c4241f20c92c9fd0ca6c97f2eb8 Mon Sep 17 00:00:00 2001 From: neha khan Date: Mon, 2 Nov 2026 17:05:17 +0530 Subject: [PATCH] Do not follow symlinks in forceDeleteOnExit and deleteDirectory forceDeleteOnExit recursed through File.isDirectory(), which follows symbolic links, so a link to a directory outside the tree got every file under its target scheduled for deletion on exit. deleteDirectory did the same on its fallback path when the link itself could not be removed. Skip the recursion for symbolic links in both places. --- .../org/codehaus/plexus/util/FileUtils.java | 8 ++- .../codehaus/plexus/util/FileUtilsTest.java | 63 +++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/codehaus/plexus/util/FileUtils.java b/src/main/java/org/codehaus/plexus/util/FileUtils.java index b411272a..bb935b1e 100644 --- a/src/main/java/org/codehaus/plexus/util/FileUtils.java +++ b/src/main/java/org/codehaus/plexus/util/FileUtils.java @@ -1247,7 +1247,9 @@ private static void deleteDirectoryOnExit(final File directory) throws IOExcepti } directory.deleteOnExit(); // The hook reverses the list - cleanDirectoryOnExit(directory); + if (!NioFiles.isSymbolicLink(directory)) { + cleanDirectoryOnExit(directory); + } } /** @@ -1343,7 +1345,9 @@ public static void deleteDirectory(final File directory) throws IOException { return; } - cleanDirectory(directory); + if (!NioFiles.isSymbolicLink(directory)) { + cleanDirectory(directory); + } if (!directory.delete()) { final String message = "Directory " + directory + " unable to be deleted."; throw new IOException(message); diff --git a/src/test/java/org/codehaus/plexus/util/FileUtilsTest.java b/src/test/java/org/codehaus/plexus/util/FileUtilsTest.java index 5bdaf97c..77f187b9 100644 --- a/src/test/java/org/codehaus/plexus/util/FileUtilsTest.java +++ b/src/test/java/org/codehaus/plexus/util/FileUtilsTest.java @@ -29,13 +29,16 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.util.Properties; +import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.condition.OS; import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.assumeFalse; /** * This is used to test FileUtils for correctness. @@ -1185,6 +1188,66 @@ void deleteDirectoryWithDanglingSymlink() throws Exception { assertFalse(getTestDirectory().exists(), "Failed to delete test directory"); } + @Test + @DisabledOnOs(OS.WINDOWS) + void deleteDirectoryDoesNotFollowDirSymlink() throws Exception { + File outside = new File(getTestDirectory(), "outside"); + outside.mkdir(); + File outsideFile = new File(outside, "keep.txt"); + createFile(outsideFile, 1); + + File directory = new File(getTestDirectory(), "directory"); + File readOnly = new File(directory, "readOnly"); + readOnly.mkdirs(); + File symlink = new File(readOnly, "dirSymlink"); + createSymlink(symlink, outside); + // the symlink cannot be unlinked from a read-only directory + readOnly.setWritable(false); + try { + assumeFalse(Files.isWritable(readOnly.toPath()), "directory permissions are not enforced"); + assertThrows(IOException.class, () -> FileUtils.deleteDirectory(directory)); + } finally { + readOnly.setWritable(true); + symlink.delete(); + } + assertTrue(outsideFile.exists(), "deleteDirectory followed a symlink out of the directory"); + } + + @Test + @DisabledOnOs(OS.WINDOWS) + void forceDeleteOnExitDoesNotFollowDirSymlink() throws Exception { + File outside = new File(getTestDirectory(), "outside"); + outside.mkdir(); + File outsideFile = new File(outside, "keep.txt"); + createFile(outsideFile, 1); + + File directory = new File(getTestDirectory(), "directory"); + directory.mkdir(); + createSymlink(new File(directory, "dirSymlink"), outside); + + // deleteOnExit only runs when the JVM exits, so run it in a separate one + String java = new File(System.getProperty("java.home"), "bin/java").getPath(); + Process process = new ProcessBuilder( + java, + "-cp", + System.getProperty("java.class.path"), + ForceDeleteOnExit.class.getName(), + directory.getAbsolutePath()) + .inheritIO() + .start(); + assertTrue(process.waitFor(1, TimeUnit.MINUTES), "JVM did not exit"); + assertEquals(0, process.exitValue()); + + assertFalse(directory.exists(), "Failed to delete directory on exit"); + assertTrue(outsideFile.exists(), "forceDeleteOnExit followed a symlink out of the directory"); + } + + public static final class ForceDeleteOnExit { + public static void main(String[] args) throws IOException { + FileUtils.forceDeleteOnExit(new File(args[0])); + } + } + @Test void testcopyDirectoryLayoutWithExcludesIncludes() throws Exception { File destination = new File("target", "copyDirectoryStructureWithExcludesIncludes");