Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;

public final class UnsafePathChecker {
private UnsafePathChecker() {}

private static final Pattern CURRENT_DIR_PATTERN = Pattern.compile("/(\\./)+");

private static final List<String> LINUX_ROOT_FOLDERS = Arrays.asList(
"/bin/",
"/boot/",
Expand Down Expand Up @@ -33,7 +37,7 @@ private UnsafePathChecker() {}
);

public static boolean startsWithUnsafePath(String filePathRaw) {
String filePath = ensureOneLeadingSlash(filePathRaw.toLowerCase());
String filePath = normalizePath(filePathRaw.toLowerCase());
Comment thread
bitterpanda63 marked this conversation as resolved.

List<String> dangerousStartsList = new ArrayList<>(DANGEROUS_PATH_STARTS);
dangerousStartsList.addAll(LINUX_ROOT_FOLDERS);
Expand All @@ -47,15 +51,24 @@ public static boolean startsWithUnsafePath(String filePathRaw) {
}

public static boolean startsWithUnsafePath(String filePathRaw, String userInputRaw) {
String filePath = ensureOneLeadingSlash(filePathRaw.toLowerCase());
String userInput = ensureOneLeadingSlash(userInputRaw.toLowerCase());
String filePath = normalizePath(filePathRaw.toLowerCase());
String userInput = normalizePath(userInputRaw.toLowerCase());
return startsWithUnsafePath(filePath) && filePath.startsWith(userInput);
}

private static String ensureOneLeadingSlash(String path) {
if (path.startsWith("/")) {
return "/" + path.replaceAll("^/+", "");
/**
* Normalizes a path by removing /./ and removing consecutive slashes
*/
private static String normalizePath(String path) {
if (path == null || path.isEmpty()) {
return path;
}
return path;

// Matches /./ or /././ or /./././ etc. (one or more ./ sequences after a /)
String normalized = CURRENT_DIR_PATTERN.matcher(path).replaceAll("/");

// Merge consecutive slashes since these don't change where you are in the path.
normalized = normalized.replaceAll("/+", "/");
return normalized;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,36 @@ public void testMultipleSlashes() {
assertTrue(UnsafePathChecker.startsWithUnsafePath("///etc/passwd"));
assertFalse(UnsafePathChecker.startsWithUnsafePath("etc/passwd///../test.txt"));
}

@Test
public void testCurrentDirectoryReferences() {
// /./ should be normalized to /
assertTrue(UnsafePathChecker.startsWithUnsafePath("/./etc/passwd", "/./etc"));
assertTrue(UnsafePathChecker.startsWithUnsafePath("/etc/./passwd", "/etc"));
assertTrue(UnsafePathChecker.startsWithUnsafePath("/etc/./passwd", "/etc/./"));

assertTrue(UnsafePathChecker.startsWithUnsafePath("/./etc/passwd"));
assertTrue(UnsafePathChecker.startsWithUnsafePath("/etc/./passwd"));
assertTrue(UnsafePathChecker.startsWithUnsafePath("/./etc/./passwd"));

// Multiple /./ sequences
assertTrue(UnsafePathChecker.startsWithUnsafePath("/././etc/passwd", "/././etc"));
assertTrue(UnsafePathChecker.startsWithUnsafePath("/etc/././passwd"));
}

@Test
public void testPathNormalization() {
// Paths with multiple slashes and /./ should be normalized and detected
assertTrue(UnsafePathChecker.startsWithUnsafePath("//etc//passwd", "/etc"));
assertTrue(UnsafePathChecker.startsWithUnsafePath("/./etc/./passwd", "/etc"));
assertTrue(UnsafePathChecker.startsWithUnsafePath("/././etc/passwd", "/etc"));

// Paths without leading slash are not unsafe
assertFalse(UnsafePathChecker.startsWithUnsafePath("etc/passwd", "etc"));
assertFalse(UnsafePathChecker.startsWithUnsafePath("", ""));

// Combined slashes and dot: ///.///etc/passwd should normalize to /etc/passwd
assertTrue(UnsafePathChecker.startsWithUnsafePath("///.///etc/passwd", "///.///etc"));
assertTrue(UnsafePathChecker.startsWithUnsafePath("///.///etc/passwd"));
}
}