Skip to content

Commit c6ca7a5

Browse files
committed
Polish "Prevent extracting zip entries outside of destination path"
See gh-25505
1 parent 2993e68 commit c6ca7a5

File tree

2 files changed

+39
-11
lines changed
  • spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src

2 files changed

+39
-11
lines changed

spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/ExtractCommand.java

+12-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -29,7 +29,6 @@
2929

3030
import org.springframework.util.Assert;
3131
import org.springframework.util.StreamUtils;
32-
import org.springframework.util.StringUtils;
3332

3433
/**
3534
* The {@code 'extract'} tools command.
@@ -86,15 +85,18 @@ protected void run(Map<Option, String> options, List<String> parameters) {
8685
}
8786

8887
private void write(ZipInputStream zip, ZipEntry entry, File destination) throws IOException {
89-
String path = StringUtils.cleanPath(entry.getName());
90-
File file = new File(destination, path);
91-
if (file.getCanonicalPath().startsWith(destination.getCanonicalPath() + File.separator)) {
92-
mkParentDirs(file);
93-
try (OutputStream out = new FileOutputStream(file)) {
94-
StreamUtils.copy(zip, out);
95-
}
96-
Files.setAttribute(file.toPath(), "creationTime", entry.getCreationTime());
88+
String canonicalOutputPath = destination.getCanonicalPath() + File.separator;
89+
File file = new File(destination, entry.getName());
90+
String canonicalEntryPath = file.getCanonicalPath();
91+
Assert.state(canonicalEntryPath.startsWith(canonicalOutputPath),
92+
() -> "Entry '" + entry.getName() + "' would be written to '" + canonicalEntryPath
93+
+ "'. This is outside the output location of '" + canonicalOutputPath
94+
+ "'. Verify the contents of your archive.");
95+
mkParentDirs(file);
96+
try (OutputStream out = new FileOutputStream(file)) {
97+
StreamUtils.copy(zip, out);
9798
}
99+
Files.setAttribute(file.toPath(), "creationTime", entry.getCreationTime());
98100
}
99101

100102
private void mkParentDirs(File file) throws IOException {

spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ExtractCommandTests.java

+27-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@
2323
import java.util.Arrays;
2424
import java.util.Collections;
2525
import java.util.Iterator;
26+
import java.util.function.Consumer;
2627
import java.util.zip.ZipEntry;
2728
import java.util.zip.ZipOutputStream;
2829

@@ -40,6 +41,7 @@
4041
* Tests for {@link ExtractCommand}.
4142
*
4243
* @author Phillip Webb
44+
* @author Andy Wilkinson
4345
*/
4446
class ExtractCommandTests {
4547

@@ -76,6 +78,7 @@ void runExtractsLayers() throws Exception {
7678
assertThat(new File(this.extract, "b/b/b.jar")).exists();
7779
assertThat(new File(this.extract, "c/c/c.jar")).exists();
7880
assertThat(new File(this.extract, "d")).isDirectory();
81+
assertThat(new File(this.extract.getParentFile(), "e.jar")).doesNotExist();
7982
}
8083

8184
@Test
@@ -95,6 +98,7 @@ void runWhenHasLayerParamsExtractsLimitedLayers() {
9598
assertThat(this.extract.list()).containsOnly("a", "c");
9699
assertThat(new File(this.extract, "a/a/a.jar")).exists();
97100
assertThat(new File(this.extract, "c/c/c.jar")).exists();
101+
assertThat(new File(this.extract.getParentFile(), "e.jar")).doesNotExist();
98102
}
99103

100104
@Test
@@ -110,7 +114,28 @@ void runWithJarFileContainingNoEntriesFails() throws IOException {
110114
.withMessageContaining("not compatible with layertools");
111115
}
112116

117+
@Test
118+
void runWithJarFileThatWouldWriteEntriesOutsideDestinationFails() throws IOException {
119+
this.jarFile = createJarFile("test.jar", (out) -> {
120+
try {
121+
out.putNextEntry(new ZipEntry("e/../../e.jar"));
122+
out.closeEntry();
123+
}
124+
catch (IOException ex) {
125+
throw new IllegalStateException(ex);
126+
}
127+
});
128+
assertThatIllegalStateException()
129+
.isThrownBy(() -> this.command.run(Collections.emptyMap(), Collections.emptyList()))
130+
.withMessageContaining("Entry 'e/../../e.jar' would be written");
131+
}
132+
113133
private File createJarFile(String name) throws IOException {
134+
return createJarFile(name, (out) -> {
135+
});
136+
}
137+
138+
private File createJarFile(String name, Consumer<ZipOutputStream> streamHandler) throws IOException {
114139
File file = new File(this.temp, name);
115140
try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(file))) {
116141
out.putNextEntry(new ZipEntry("a/"));
@@ -127,6 +152,7 @@ private File createJarFile(String name) throws IOException {
127152
out.closeEntry();
128153
out.putNextEntry(new ZipEntry("d/"));
129154
out.closeEntry();
155+
streamHandler.accept(out);
130156
}
131157
return file;
132158
}

0 commit comments

Comments
 (0)