Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make sure DirectoryPathTree does not allow reading outside the root dir #23692

Merged
merged 1 commit into from
Feb 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions independent-projects/bootstrap/app-model/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>io.quarkus</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public void walk(PathVisitor visitor) {

@Override
protected <T> T apply(String relativePath, Function<PathVisit, T> func, boolean manifestEnabled) {
DirectoryPathTree.ensureRelativePath(relativePath);
if (!PathFilter.isVisible(pathFilter, relativePath)) {
return func.apply(null);
}
Expand All @@ -73,6 +74,7 @@ protected <T> T apply(String relativePath, Function<PathVisit, T> func, boolean

@Override
public void accept(String relativePath, Consumer<PathVisit> consumer) {
DirectoryPathTree.ensureRelativePath(relativePath);
if (!PathFilter.isVisible(pathFilter, relativePath)) {
consumer.accept(null);
return;
Expand All @@ -97,6 +99,7 @@ public void accept(String relativePath, Consumer<PathVisit> consumer) {

@Override
public boolean contains(String relativePath) {
DirectoryPathTree.ensureRelativePath(relativePath);
if (!PathFilter.isVisible(pathFilter, relativePath)) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.io.IOException;
import java.io.Serializable;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
Expand All @@ -10,11 +11,35 @@
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Pattern;

public class DirectoryPathTree extends PathTreeWithManifest implements OpenPathTree, Serializable {

private static final long serialVersionUID = 2255956884896445059L;

private static final boolean USE_WINDOWS_ABSOLUTE_PATH_PATTERN = !FileSystems.getDefault().getSeparator().equals("/");

private static volatile Pattern windowsAbsolutePathPattern;

private static Pattern windowsAbsolutePathPattern() {
return windowsAbsolutePathPattern == null ? windowsAbsolutePathPattern = Pattern.compile("[a-zA-Z]:\\\\.*")
: windowsAbsolutePathPattern;
}

static boolean isAbsolutePath(String path) {
return path != null && !path.isEmpty()
&& (path.charAt(0) == '/' // we want to check for '/' on every OS
|| USE_WINDOWS_ABSOLUTE_PATH_PATTERN
&& (windowsAbsolutePathPattern().matcher(path).matches())
|| path.startsWith(FileSystems.getDefault().getSeparator()));
}

static void ensureRelativePath(String path) {
if (isAbsolutePath(path)) {
throw new IllegalArgumentException("Expected a path relative to the root of the path tree but got " + path);
}
}

private Path dir;
private PathFilter pathFilter;

Expand Down Expand Up @@ -55,8 +80,22 @@ public void walk(PathVisitor visitor) {
PathTreeVisit.walk(dir, dir, pathFilter, getMultiReleaseMapping(), visitor);
}

private String normalize(String path) {
ensureRelativePath(path);
// this is to disallow reading outside the path tree root
if (path != null && path.contains("..")) {
final Path absolutePath = dir.resolve(path).normalize().toAbsolutePath();
if (absolutePath.startsWith(dir)) {
return dir.relativize(absolutePath).toString();
}
return null;
}
return path;
}

@Override
protected <T> T apply(String relativePath, Function<PathVisit, T> func, boolean manifestEnabled) {
relativePath = normalize(relativePath);
if (!PathFilter.isVisible(pathFilter, relativePath)) {
return func.apply(null);
}
Expand All @@ -69,6 +108,7 @@ protected <T> T apply(String relativePath, Function<PathVisit, T> func, boolean

@Override
public void accept(String relativePath, Consumer<PathVisit> consumer) {
relativePath = normalize(relativePath);
if (!PathFilter.isVisible(pathFilter, relativePath)) {
consumer.accept(null);
return;
Expand All @@ -83,6 +123,7 @@ public void accept(String relativePath, Consumer<PathVisit> consumer) {

@Override
public boolean contains(String relativePath) {
relativePath = normalize(relativePath);
if (!PathFilter.isVisible(pathFilter, relativePath)) {
return false;
}
Expand All @@ -92,6 +133,7 @@ public boolean contains(String relativePath) {

@Override
public Path getPath(String relativePath) {
relativePath = normalize(relativePath);
if (!PathFilter.isVisible(pathFilter, relativePath)) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ public class PathFilter implements Serializable {
private static final long serialVersionUID = -5712472676677054175L;

public static boolean isVisible(PathFilter filter, String path) {
if (path == null) {
return false;
}
if (filter == null) {
return true;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package io.quarkus.paths;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Set;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

public class DirectoryPathTreeTest {

private static final String BASE_DIR = "paths/directory-path-tree";

private static volatile Path baseDir;

@BeforeAll
public static void staticInit() throws Exception {
final URL url = Thread.currentThread().getContextClassLoader().getResource(BASE_DIR);
if (url == null) {
throw new IllegalStateException("Failed to locate " + BASE_DIR + " on the classpath");
}
baseDir = Path.of(url.toURI()).toAbsolutePath();
if (!Files.exists(baseDir)) {
throw new IllegalStateException("Failed to locate " + baseDir);
}
}

@Test
public void acceptExistingPath() throws Exception {
final Path root = resolveTreeRoot("root");
final PathTree tree = PathTree.ofDirectoryOrArchive(root);
tree.accept("README.md", visit -> {
assertThat(visit).isNotNull();
assertThat(visit.getRelativePath("/")).isEqualTo("README.md");
assertThat(visit.getPath()).exists();
assertThat(visit.getRoot()).isEqualTo(root);
try {
assertThat(Files.readString(visit.getPath())).isEqualTo("test readme");
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}

@Test
public void acceptNonExistentPath() throws Exception {
final Path root = resolveTreeRoot("root");
final PathTree tree = PathTree.ofDirectoryOrArchive(root);
tree.accept("non-existent", visit -> {
assertThat(visit).isNull();
});
}

@Test
public void acceptAbsolutePath() throws Exception {
final Path root = resolveTreeRoot("root");
final PathTree tree = PathTree.ofDirectoryOrArchive(root);
try {
tree.accept("/README.md", visit -> {
fail("Absolute paths aren't allwed");
});
} catch (IllegalArgumentException e) {
// expected
}
}

@Test
public void acceptIllegalAbsolutePathOutsideTree() throws Exception {
final Path root = resolveTreeRoot("root");
final PathTree tree = PathTree.ofDirectoryOrArchive(root);
final Path absolute = root.getParent().resolve("external.txt");
assertThat(absolute).exists();
try {
tree.accept(absolute.toString(), visit -> {
fail("Absolute paths aren't allwed");
});
} catch (IllegalArgumentException e) {
// expected
}
}

@Test
public void acceptExistingRelativeNonNormalizedPath() throws Exception {
final Path root = resolveTreeRoot("root");
final PathTree tree = PathTree.ofDirectoryOrArchive(root);
tree.accept("../root/./other/../README.md", visit -> {
assertThat(visit).isNotNull();
assertThat(visit.getRelativePath("/")).isEqualTo("README.md");
assertThat(visit.getPath()).exists();
assertThat(visit.getRoot()).isEqualTo(root);
try {
assertThat(Files.readString(visit.getPath())).isEqualTo("test readme");
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}

@Test
public void acceptNonExistentRelativeNonNormalizedPath() throws Exception {
final Path root = resolveTreeRoot("root");
final PathTree tree = PathTree.ofDirectoryOrArchive(root);
tree.accept("../root/./README.md/../non-existent.txt", visit -> {
assertThat(visit).isNull();
});
}

@Test
public void walk() throws Exception {
final Path root = resolveTreeRoot("root");
final PathTree tree = PathTree.ofDirectoryOrArchive(root);

final Set<String> visited = new HashSet<>();
final PathVisitor visitor = new PathVisitor() {
@Override
public void visitPath(PathVisit visit) {
visited.add(visit.getRelativePath("/"));
}
};
tree.walk(visitor);

assertThat(visited).isEqualTo(Set.of(
"",
"README.md",
"src",
"src/main",
"src/main/java",
"src/main/java/Main.java"));
}

/**
* Returns a path relative to src/test/resources/paths/directory-path-tree/
*
* @param relative relative path
* @return Path instance
*/
private Path resolveTreeRoot(String relative) {
return baseDir.resolve(relative);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package io.quarkus.paths;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail;

import io.quarkus.fs.util.ZipUtils;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Set;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

public class JarPathTreeTest {

private static final String BASE_DIR = "paths/directory-path-tree";

private static Path root;

@BeforeAll
public static void staticInit() throws Exception {
final URL url = Thread.currentThread().getContextClassLoader().getResource(BASE_DIR + "/root");
if (url == null) {
throw new IllegalStateException("Failed to locate " + BASE_DIR + " on the classpath");
}
final Path rootDir = Path.of(url.toURI()).toAbsolutePath();
if (!Files.exists(rootDir)) {
throw new IllegalStateException("Failed to locate " + rootDir);
}

root = rootDir.getParent().resolve("root.jar");
ZipUtils.zip(rootDir, root);
}

@Test
public void acceptExistingPath() throws Exception {
final PathTree tree = PathTree.ofDirectoryOrArchive(root);
tree.accept("README.md", visit -> {
assertThat(visit).isNotNull();
assertThat(visit.getRelativePath("/")).isEqualTo("README.md");
assertThat(visit.getPath()).exists();
assertThat(visit.getRoot()).isEqualTo(root);
try {
assertThat(Files.readString(visit.getPath())).isEqualTo("test readme");
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}

@Test
public void acceptNonExistentPath() throws Exception {
final PathTree tree = PathTree.ofDirectoryOrArchive(root);
tree.accept("non-existent", visit -> {
assertThat(visit).isNull();
});
}

@Test
public void acceptAbsolutePath() throws Exception {
final PathTree tree = PathTree.ofDirectoryOrArchive(root);
try {
tree.accept("/README.md", visit -> {
fail("Absolute paths aren't allowed");
});
} catch (IllegalArgumentException e) {
//expected
}
}

@Test
public void acceptAbsolutePathOutsideTree() throws Exception {
final PathTree tree = PathTree.ofDirectoryOrArchive(root);
final Path absolute = root.getParent().resolve("external.txt");
assertThat(absolute).exists();
try {
tree.accept(absolute.toString(), visit -> {
fail("Absolute paths aren't allowed");
});
} catch (IllegalArgumentException e) {
//expected
}
}

@Test
public void walk() throws Exception {
final PathTree tree = PathTree.ofDirectoryOrArchive(root);

final Set<String> visited = new HashSet<>();
final PathVisitor visitor = new PathVisitor() {
@Override
public void visitPath(PathVisit visit) {
visited.add(visit.getRelativePath("/"));
}
};
tree.walk(visitor);

assertThat(visited).isEqualTo(Set.of(
"",
"README.md",
"src",
"src/main",
"src/main/java",
"src/main/java/Main.java"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
external
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test readme
Loading