From 700c978bfbddab4143a0d75f31456da2b58e4d4e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 20 Apr 2026 10:03:37 +0100 Subject: [PATCH] Normalize layer directory locations Closes gh-50130 --- .../boot/jarmode/tools/ExtractCommand.java | 23 ++++++++++----- .../tools/ExtractLayersCommandTests.java | 28 +++++++++++++++++++ 2 files changed, 44 insertions(+), 7 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 86ed936e390..29f68219f5e 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 @@ -489,8 +489,8 @@ class ExtractCommand extends Command { public File resolve(String originalName, String newName) throws IOException { String layer = this.layers.getLayer(originalName); if (shouldExtractLayer(layer)) { - File directory = getLayerDirectory(layer); - return assertFileIsContainedInDirectory(directory, new File(directory, newName), newName); + File layerDirectory = getLayerDirectory(layer); + return assertFileIsContainedInDirectory(layerDirectory, new File(layerDirectory, newName), newName); } return null; } @@ -499,21 +499,30 @@ class ExtractCommand extends Command { public File resolveApplication() throws IOException { String layer = this.layers.getApplicationLayerName(); if (shouldExtractLayer(layer)) { - File directory = getLayerDirectory(layer); - return assertFileIsContainedInDirectory(directory, new File(directory, this.applicationFilename), - this.applicationFilename); + File layerDirectory = getLayerDirectory(layer); + return assertFileIsContainedInDirectory(layerDirectory, + new File(layerDirectory, this.applicationFilename), this.applicationFilename); } return null; } - private File getLayerDirectory(String layer) { - return new File(this.directory, layer); + private File getLayerDirectory(String layer) throws IOException { + return assertLayerDirectoryLocation(new File(this.directory, layer), layer); } private boolean shouldExtractLayer(String layer) { return this.layersToExtract.isEmpty() || this.layersToExtract.contains(layer); } + 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), + () -> "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/ExtractLayersCommandTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractLayersCommandTests.java index 3979dac0522..e77cc02e391 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractLayersCommandTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractLayersCommandTests.java @@ -172,6 +172,34 @@ class ExtractLayersCommandTests { .withMessageContaining("Entry 'e/../../e.jar' would be written"); } + @Test + void runWithLayerNameThatWouldWriteEntriesOutsideDestinationFails() throws Exception { + this.command = new ExtractLayersCommand(this.context, new Layers() { + + @Override + public Iterator iterator() { + return Arrays.asList("a", "b", "c").iterator(); + } + + @Override + public String getLayer(String entryName) { + return "../../outside-layer"; + } + + @Override + public String getApplicationLayerName() { + return "application"; + } + + }); + this.jarFile = createJarFile("test.jar"); + given(this.context.getArchiveFile()).willReturn(this.jarFile); + given(this.context.getWorkingDir()).willReturn(this.extract); + assertThatIllegalStateException() + .isThrownBy(() -> this.command.run(System.out, Collections.emptyMap(), Collections.emptyList())) + .withMessageContaining("Layer '../../outside-layer' would be written to"); + } + private File createJarFile(String name) throws Exception { return createJarFile(name, (out) -> { });