diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/ExtractCommand.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/ExtractCommand.java index f3a49211288..e59f44c80a1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/ExtractCommand.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/ExtractCommand.java @@ -29,7 +29,6 @@ import java.util.zip.ZipInputStream; import org.springframework.util.Assert; import org.springframework.util.StreamUtils; -import org.springframework.util.StringUtils; /** * The {@code 'extract'} tools command. @@ -86,15 +85,18 @@ class ExtractCommand extends Command { } private void write(ZipInputStream zip, ZipEntry entry, File destination) throws IOException { - String path = StringUtils.cleanPath(entry.getName()); - File file = new File(destination, path); - if (file.getAbsolutePath().startsWith(destination.getAbsolutePath())) { - mkParentDirs(file); - try (OutputStream out = new FileOutputStream(file)) { - StreamUtils.copy(zip, out); - } - Files.setAttribute(file.toPath(), "creationTime", entry.getCreationTime()); + String canonicalOutputPath = destination.getCanonicalPath() + File.separator; + File file = new File(destination, entry.getName()); + String canonicalEntryPath = file.getCanonicalPath(); + Assert.state(canonicalEntryPath.startsWith(canonicalOutputPath), + () -> "Entry '" + entry.getName() + "' would be written to '" + canonicalEntryPath + + "'. This is outside the output location of '" + canonicalOutputPath + + "'. Verify the contents of your archive."); + mkParentDirs(file); + try (OutputStream out = new FileOutputStream(file)) { + StreamUtils.copy(zip, out); } + Files.setAttribute(file.toPath(), "creationTime", entry.getCreationTime()); } private void mkParentDirs(File file) throws IOException { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ExtractCommandTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ExtractCommandTests.java index 40058992f99..357a45b3596 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ExtractCommandTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ExtractCommandTests.java @@ -24,6 +24,7 @@ import java.io.InputStreamReader; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; +import java.util.function.Consumer; import java.util.jar.JarEntry; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -46,6 +47,7 @@ import static org.mockito.BDDMockito.given; * Tests for {@link ExtractCommand}. * * @author Phillip Webb + * @author Andy Wilkinson */ @ExtendWith(MockitoExtension.class) class ExtractCommandTests { @@ -82,6 +84,7 @@ class ExtractCommandTests { assertThat(new File(this.extract, "b/b/b.jar")).exists(); assertThat(new File(this.extract, "c/c/c.jar")).exists(); assertThat(new File(this.extract, "d")).isDirectory(); + assertThat(new File(this.extract.getParentFile(), "e.jar")).doesNotExist(); } @Test @@ -104,6 +107,7 @@ class ExtractCommandTests { assertThat(this.extract.list()).containsOnly("a", "c"); assertThat(new File(this.extract, "a/a/a.jar")).exists(); assertThat(new File(this.extract, "c/c/c.jar")).exists(); + assertThat(new File(this.extract.getParentFile(), "e.jar")).doesNotExist(); } @Test @@ -119,7 +123,29 @@ class ExtractCommandTests { .withMessageContaining("not compatible with layertools"); } + @Test + void runWithJarFileThatWouldWriteEntriesOutsideDestinationFails() throws Exception { + this.jarFile = createJarFile("test.jar", (out) -> { + try { + out.putNextEntry(new ZipEntry("e/../../e.jar")); + out.closeEntry(); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + }); + given(this.context.getArchiveFile()).willReturn(this.jarFile); + assertThatIllegalStateException() + .isThrownBy(() -> this.command.run(Collections.emptyMap(), Collections.emptyList())) + .withMessageContaining("Entry 'e/../../e.jar' would be written"); + } + private File createJarFile(String name) throws Exception { + return createJarFile(name, (out) -> { + }); + } + + private File createJarFile(String name, Consumer streamHandler) throws Exception { File file = new File(this.temp, name); try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(file))) { out.putNextEntry(new ZipEntry("a/")); @@ -139,6 +165,7 @@ class ExtractCommandTests { out.putNextEntry(new JarEntry("META-INF/MANIFEST.MF")); out.write(getFile("test-manifest.MF").getBytes()); out.closeEntry(); + streamHandler.accept(out); } return file; }