From f7343398d4cb7621d6ee1dadc321b71d3f6b256e Mon Sep 17 00:00:00 2001 From: Dongliang Xie Date: Tue, 26 May 2026 02:05:16 +0800 Subject: [PATCH 1/2] Handle root output directory when extracting layers Use canonical Path containment checks so layer directories under the filesystem root are accepted while entries and layer names that escape the output directory remain rejected. See gh-50501 Signed-off-by: Dongliang Xie --- .../boot/jarmode/tools/ExtractCommand.java | 18 ++++++---- .../jarmode/tools/ExtractCommandTests.java | 35 +++++++++++++++++++ 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java index 29f68219f5e..7103fcc3d5a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java @@ -26,6 +26,7 @@ import java.io.OutputStream; import java.io.PrintStream; import java.io.UncheckedIOException; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; @@ -55,6 +56,7 @@ import org.springframework.util.StringUtils; * The {@code 'extract'} tools command. * * @author Moritz Halbritter + * @author Dongliang Xie */ class ExtractCommand extends Command { @@ -363,14 +365,18 @@ class ExtractCommand extends Command { } private static File assertFileIsContainedInDirectory(File directory, File file, String name) throws IOException { - String canonicalOutputPath = directory.getCanonicalPath() + File.separator; - String canonicalEntryPath = file.getCanonicalPath(); - Assert.state(canonicalEntryPath.startsWith(canonicalOutputPath), + Path canonicalOutputPath = directory.getCanonicalFile().toPath(); + Path canonicalEntryPath = file.getCanonicalFile().toPath(); + Assert.state(isFileContainedInDirectory(canonicalOutputPath, canonicalEntryPath), () -> "Entry '%s' would be written to '%s'. This is outside the output location of '%s'. Verify the contents of your archive." .formatted(name, canonicalEntryPath, canonicalOutputPath)); return file; } + private static boolean isFileContainedInDirectory(Path canonicalOutputPath, Path canonicalFilePath) { + return !canonicalFilePath.equals(canonicalOutputPath) && canonicalFilePath.startsWith(canonicalOutputPath); + } + @FunctionalInterface private interface EntryNameTransformer { @@ -515,9 +521,9 @@ class ExtractCommand extends Command { } private File assertLayerDirectoryLocation(File layerDirectory, String layerName) throws IOException { - String canonicalOutputPath = this.directory.getCanonicalPath() + File.separator; - String canonicalLayerPath = layerDirectory.getCanonicalPath(); - Assert.state(canonicalLayerPath.startsWith(canonicalOutputPath), + Path canonicalOutputPath = this.directory.getCanonicalFile().toPath(); + Path canonicalLayerPath = layerDirectory.getCanonicalFile().toPath(); + Assert.state(isFileContainedInDirectory(canonicalOutputPath, canonicalLayerPath), () -> "Layer '%s' would be written to '%s'. This is outside the output location of '%s'. Verify the contents of your archive." .formatted(layerName, canonicalLayerPath, canonicalOutputPath)); return layerDirectory; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractCommandTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractCommandTests.java index 490ab506059..a1a6003cd00 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractCommandTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractCommandTests.java @@ -20,11 +20,13 @@ import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.BasicFileAttributes; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Enumeration; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.jar.Manifest; @@ -45,6 +47,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; * Tests for {@link ExtractCommand}. * * @author Moritz Halbritter + * @author Dongliang Xie */ class ExtractCommandTests extends AbstractJarModeTests { @@ -370,6 +373,38 @@ class ExtractCommandTests extends AbstractJarModeTests { .doesNotContain("test/spring-boot-loader/org/springframework/boot/loader/launch/JarLauncher.class"); } + @Test + void extractWhenDestinationIsFileSystemRoot() throws IOException { + Path layerDirectory = ExtractCommandTests.this.tempDir.toPath() + .resolve("root-output") + .resolve("dependencies") + .toAbsolutePath() + .normalize(); + Path outputRoot = layerDirectory.getRoot(); + String layerName = outputRoot.relativize(layerDirectory).toString().replace(File.separatorChar, '/'); + Layers layers = new Layers() { + + @Override + public Iterator iterator() { + return List.of(layerName).iterator(); + } + + @Override + public String getLayer(String entryName) { + return layerName; + } + + @Override + public String getApplicationLayerName() { + return layerName; + } + + }; + runCommand((context) -> new ExtractCommand(context, layers), ExtractCommandTests.this.archive, + "--destination", outputRoot.toString(), "--force", "--launcher", "--layers"); + assertThat(layerDirectory.resolve("BOOT-INF/lib/dependency-1.jar")).exists(); + } + } } From db1ebf59ba83026601117b91d4a6571a4fcc8401 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 27 May 2026 10:33:19 +0200 Subject: [PATCH 2/2] Polish "Handle root output directory when extracting layers" See gh-50501 --- .../springframework/boot/jarmode/tools/ExtractCommand.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java index 7103fcc3d5a..c2d0bb0ad9c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java @@ -373,8 +373,8 @@ class ExtractCommand extends Command { return file; } - private static boolean isFileContainedInDirectory(Path canonicalOutputPath, Path canonicalFilePath) { - return !canonicalFilePath.equals(canonicalOutputPath) && canonicalFilePath.startsWith(canonicalOutputPath); + private static boolean isFileContainedInDirectory(Path directory, Path file) { + return !file.equals(directory) && file.startsWith(directory); } @FunctionalInterface