Skip to content

Commit f734339

Browse files
DragonFSKYmhalbritter
authored andcommitted
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 <dragonfsky@gmail.com>
1 parent bfd1b8c commit f734339

2 files changed

Lines changed: 47 additions & 6 deletions

File tree

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.io.PrintStream;
2727
import java.io.UncheckedIOException;
2828
import java.nio.file.Files;
29+
import java.nio.file.Path;
2930
import java.nio.file.attribute.BasicFileAttributeView;
3031
import java.nio.file.attribute.BasicFileAttributes;
3132
import java.nio.file.attribute.FileTime;
@@ -55,6 +56,7 @@
5556
* The {@code 'extract'} tools command.
5657
*
5758
* @author Moritz Halbritter
59+
* @author Dongliang Xie
5860
*/
5961
class ExtractCommand extends Command {
6062

@@ -363,14 +365,18 @@ private static void withJarEntries(File file, ManfiestWriter manfiestWriter, Thr
363365
}
364366

365367
private static File assertFileIsContainedInDirectory(File directory, File file, String name) throws IOException {
366-
String canonicalOutputPath = directory.getCanonicalPath() + File.separator;
367-
String canonicalEntryPath = file.getCanonicalPath();
368-
Assert.state(canonicalEntryPath.startsWith(canonicalOutputPath),
368+
Path canonicalOutputPath = directory.getCanonicalFile().toPath();
369+
Path canonicalEntryPath = file.getCanonicalFile().toPath();
370+
Assert.state(isFileContainedInDirectory(canonicalOutputPath, canonicalEntryPath),
369371
() -> "Entry '%s' would be written to '%s'. This is outside the output location of '%s'. Verify the contents of your archive."
370372
.formatted(name, canonicalEntryPath, canonicalOutputPath));
371373
return file;
372374
}
373375

376+
private static boolean isFileContainedInDirectory(Path canonicalOutputPath, Path canonicalFilePath) {
377+
return !canonicalFilePath.equals(canonicalOutputPath) && canonicalFilePath.startsWith(canonicalOutputPath);
378+
}
379+
374380
@FunctionalInterface
375381
private interface EntryNameTransformer {
376382

@@ -515,9 +521,9 @@ private boolean shouldExtractLayer(String layer) {
515521
}
516522

517523
private File assertLayerDirectoryLocation(File layerDirectory, String layerName) throws IOException {
518-
String canonicalOutputPath = this.directory.getCanonicalPath() + File.separator;
519-
String canonicalLayerPath = layerDirectory.getCanonicalPath();
520-
Assert.state(canonicalLayerPath.startsWith(canonicalOutputPath),
524+
Path canonicalOutputPath = this.directory.getCanonicalFile().toPath();
525+
Path canonicalLayerPath = layerDirectory.getCanonicalFile().toPath();
526+
Assert.state(isFileContainedInDirectory(canonicalOutputPath, canonicalLayerPath),
521527
() -> "Layer '%s' would be written to '%s'. This is outside the output location of '%s'. Verify the contents of your archive."
522528
.formatted(layerName, canonicalLayerPath, canonicalOutputPath));
523529
return layerDirectory;

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@
2020
import java.io.FileWriter;
2121
import java.io.IOException;
2222
import java.nio.file.Files;
23+
import java.nio.file.Path;
2324
import java.nio.file.attribute.BasicFileAttributeView;
2425
import java.nio.file.attribute.BasicFileAttributes;
2526
import java.time.Instant;
2627
import java.time.temporal.ChronoUnit;
2728
import java.util.Enumeration;
29+
import java.util.Iterator;
2830
import java.util.List;
2931
import java.util.Map;
3032
import java.util.jar.Manifest;
@@ -45,6 +47,7 @@
4547
* Tests for {@link ExtractCommand}.
4648
*
4749
* @author Moritz Halbritter
50+
* @author Dongliang Xie
4851
*/
4952
class ExtractCommandTests extends AbstractJarModeTests {
5053

@@ -370,6 +373,38 @@ void extractsOnlySelectedLayers() throws IOException {
370373
.doesNotContain("test/spring-boot-loader/org/springframework/boot/loader/launch/JarLauncher.class");
371374
}
372375

376+
@Test
377+
void extractWhenDestinationIsFileSystemRoot() throws IOException {
378+
Path layerDirectory = ExtractCommandTests.this.tempDir.toPath()
379+
.resolve("root-output")
380+
.resolve("dependencies")
381+
.toAbsolutePath()
382+
.normalize();
383+
Path outputRoot = layerDirectory.getRoot();
384+
String layerName = outputRoot.relativize(layerDirectory).toString().replace(File.separatorChar, '/');
385+
Layers layers = new Layers() {
386+
387+
@Override
388+
public Iterator<String> iterator() {
389+
return List.of(layerName).iterator();
390+
}
391+
392+
@Override
393+
public String getLayer(String entryName) {
394+
return layerName;
395+
}
396+
397+
@Override
398+
public String getApplicationLayerName() {
399+
return layerName;
400+
}
401+
402+
};
403+
runCommand((context) -> new ExtractCommand(context, layers), ExtractCommandTests.this.archive,
404+
"--destination", outputRoot.toString(), "--force", "--launcher", "--layers");
405+
assertThat(layerDirectory.resolve("BOOT-INF/lib/dependency-1.jar")).exists();
406+
}
407+
373408
}
374409

375410
}

0 commit comments

Comments
 (0)