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

Prevent path injection using ".." in object URLs #17

Merged
merged 2 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
69 changes: 54 additions & 15 deletions src/main/java/net/ripe/rpki/rsyncit/rsync/RsyncWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import net.ripe.rpki.rsyncit.rrdp.RpkiObject;
import org.apache.tomcat.util.http.fileupload.FileUtils;

import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
Expand All @@ -16,6 +17,7 @@
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -68,39 +70,50 @@ private Path writeObjectToNewDirectory(List<RpkiObject> objects, Instant now) th
final Path temporaryDirectory = Files.createTempDirectory(Paths.get(config.rsyncPath()), "tmp-" + formattedNow + "-");
try {
groupedByHost.forEach((hostName, os) -> {
// creeate a directory per hostname (in realistic cases there will be just one)
var hostDir = temporaryDirectory.resolve(hostName);
// create a directory per hostname (in realistic cases there will be just one)
var hostBasedPath = temporaryDirectory.resolve(hostName);
try {
Files.createDirectories(hostDir);
Files.createDirectories(hostBasedPath);
} catch (IOException e) {
log.error("Could not create {}", hostDir);
log.error("Could not create {}", hostBasedPath);
}

// create directories in "shortest first" order
os.stream()
.map(o ->
// Filter out objects with potentially insecure URLs
var wellBehavingObjects = filterOutBadUrls(hostBasedPath, os);

// Create directories in "shortest first" order.
// Use canonical path to avoid potential troubles with relative ".." paths
wellBehavingObjects
.stream()
.flatMap(o -> {
// remove the filename, i.e. /foo/bar/object.cer -> /foo/bar
Paths.get(relativePath(o.url().getPath())).getParent()
)
var objectParentDir = Paths.get(relativePath(o.url().getPath())).getParent();
try {
return Stream.of(hostBasedPath.resolve(objectParentDir).toFile().getCanonicalFile().toPath());
} catch (IOException e) {
log.error("Could not find a parent directory for the object {}", o.url(), e);
return Stream.empty();
}
})
.sorted()
.distinct()
.forEach(dir -> {
try {
Files.createDirectories(temporaryDirectory.resolve(hostName).resolve(dir));
Files.createDirectories(dir);
} catch (IOException ex) {
log.error("Could not create directory {}", dir, ex);
}
});

fileWriterPool.submit(() -> os.stream()
fileWriterPool.submit(() -> wellBehavingObjects.stream()
.parallel()
.forEach(o -> {
var path = Paths.get(relativePath(o.url().getPath()));
var fullPath = temporaryDirectory.resolve(hostName).resolve(path);
try {
Files.write(fullPath, o.bytes());
var canonicalFullPath = hostBasedPath.resolve(path).toFile().getCanonicalFile().toPath();
Files.write(canonicalFullPath, o.bytes());
// rsync relies on the correct timestamp for fast synchronization
Files.setLastModifiedTime(fullPath, FileTime.from(o.modificationTime()));
Files.setLastModifiedTime(canonicalFullPath, FileTime.from(o.modificationTime()));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
Expand All @@ -123,6 +136,32 @@ private Path writeObjectToNewDirectory(List<RpkiObject> objects, Instant now) th
}
}

static List<RpkiObject> filterOutBadUrls(Path hostBasedPath, Collection<RpkiObject> objects) {
final String canonicalHostPath;
try {
canonicalHostPath = hostBasedPath.toFile().getCanonicalPath();
} catch (IOException e) {
throw new RuntimeException(e);
}
return objects.stream().flatMap(object -> {
var objectRelativePath = Paths.get(relativePath(object.url().getPath()));
try {
// Check that the resulting path of the object stays within `hostBasedPath`
// to prevent URLs like rsync://bla.net/path/../../../../../PATH_INJECTION.txt
// writing data outside of the controlled path.
final String canonicalPath = hostBasedPath.resolve(objectRelativePath).toFile().getCanonicalPath();
if (canonicalPath.startsWith(canonicalHostPath)) {
return Stream.of(object);
} else {
log.error("The object with url {} was skipped.", object.url());
}
} catch (IOException e) {
log.error("The object with url {} was skipped due to the error.", object.url(), e);
}
return Stream.empty();
}).collect(Collectors.toList());
}

private void atomicallyReplacePublishedSymlink(Path baseDirectory, Path targetDirectory) throws IOException {
Path targetSymlink = baseDirectory.resolve("published");

Expand Down Expand Up @@ -171,7 +210,7 @@ private FileTime getLastModifiedTime(Path path) {
}
}

private String relativePath(final String path) {
private static String relativePath(final String path) {
if (path.startsWith("/")) {
return path.substring(1);
}
Expand Down
14 changes: 14 additions & 0 deletions src/test/java/net/ripe/rpki/rsyncit/rsync/RsyncWriterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,20 @@ public void testWriteMultipleObjects(@TempDir Path tmpPath) throws Exception {
});
}

@Test
public void testIgnoreBadUrls(@TempDir Path tmpPath) throws Exception {
withRsyncWriter(tmpPath, rsyncWriter -> {
var o1 = new RpkiObject(URI.create("rsync://bla.net/path1/a.cer"), someBytes(), Instant.now());
var o2 = new RpkiObject(URI.create("rsync://bla.net/path1/../../PATH_INJECTION.txt"), someBytes(), Instant.now());
var o3 = new RpkiObject(URI.create("rsync://bla.net/path1/path2/../NOT_REALLY_PATH_INJECTION.txt"), someBytes(), Instant.now());
lolepezy marked this conversation as resolved.
Show resolved Hide resolved
rsyncWriter.writeObjects(Arrays.asList(o1, o2, o3));
final String root = rsyncWriter.getConfig().rsyncPath();
checkFile(Paths.get(root, "published", "bla.net", "path1", "a.cer"), o1.bytes());
assertThat(Paths.get(root, "published", "PATH_INJECTION.txt").toFile().exists()).isFalse();
checkFile(Paths.get(root, "published", "bla.net", "path1", "NOT_REALLY_PATH_INJECTION.txt"), o3.bytes());
});
}

@Test
public void testRemoveOldDirectoriesWhenTheyAreOld(@TempDir Path tmpPath) throws Exception {
final Function<RsyncWriter, Path> writeSomeObjects = rsyncWriter ->
Expand Down
Loading