Skip to content

Commit 065b5b3

Browse files
garydgregoryggregory-rocket
authored andcommitted
Address relative path issues
1 parent 2692adf commit 065b5b3

File tree

2 files changed

+72
-15
lines changed

2 files changed

+72
-15
lines changed

src/main/java/org/apache/commons/io/file/PathFence.java

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -120,27 +120,37 @@ public static Builder builder() {
120120
* @param builder A builder.
121121
*/
122122
private PathFence(final Builder builder) {
123-
this.roots = Arrays.stream(builder.roots).map(Path::toAbsolutePath).collect(Collectors.toList());
123+
this.roots = Arrays.stream(builder.roots).map(this::absoluteNormalize).collect(Collectors.toList());
124124
}
125125

126126
/**
127-
* Checks that that a Path resolves within our fence.
127+
* Converts the given path to a normalized absolute path.
128+
*
129+
* @param path The source path.
130+
* @return The result path.
131+
*/
132+
private Path absoluteNormalize(final Path path) {
133+
return path.toAbsolutePath().normalize();
134+
}
135+
136+
/**
137+
* Checks that that a Path is within our fence.
128138
*
129139
* @param path The path to test.
130140
* @return The given path.
131-
* @throws IllegalArgumentException Thrown if the file name is not without our fence.
141+
* @throws IllegalArgumentException Thrown if the path is not within our fence.
132142
*/
133143
// Cannot implement both UnaryOperator<Path> and Function<String, Path>
134144
public Path apply(final Path path) {
135145
return getPath(path.toString(), path);
136146
}
137147

138148
/**
139-
* Gets a Path for the given file name, checking that it resolves within our fence.
149+
* Gets a Path for the given file name, checking that it is within our fence.
140150
*
141-
* @param fileName the file name to resolve.
142-
* @return a fenced Path.
143-
* @throws IllegalArgumentException Thrown if the file name is not without our fence.
151+
* @param fileName the file name to test.
152+
* @return The given path.
153+
* @throws IllegalArgumentException Thrown if the file name is not within our fence.
144154
*/
145155
// Cannot implement both UnaryOperator<Path> and Function<String, Path>
146156
public Path apply(final String fileName) {
@@ -151,7 +161,7 @@ private Path getPath(final String fileName, final Path path) {
151161
if (roots.isEmpty()) {
152162
return path;
153163
}
154-
final Path pathAbs = path.normalize().toAbsolutePath();
164+
final Path pathAbs = absoluteNormalize(path);
155165
final Optional<Path> first = roots.stream().filter(pathAbs::startsWith).findFirst();
156166
if (first.isPresent()) {
157167
return path;

src/test/java/org/apache/commons/io/file/PathFenceTest.java

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import static org.junit.jupiter.api.Assertions.assertEquals;
2121
import static org.junit.jupiter.api.Assertions.assertNotNull;
22+
import static org.junit.jupiter.api.Assertions.assertNull;
2223
import static org.junit.jupiter.api.Assertions.assertSame;
2324
import static org.junit.jupiter.api.Assertions.assertThrows;
2425
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -28,6 +29,7 @@
2829
import java.nio.file.Path;
2930
import java.nio.file.Paths;
3031

32+
import org.apache.commons.lang3.StringUtils;
3133
import org.junit.jupiter.api.Test;
3234
import org.junit.jupiter.api.io.TempDir;
3335
import org.junit.jupiter.params.ParameterizedTest;
@@ -42,16 +44,29 @@ private Path createDirectory(final Path tempDir, final String other) throws IOEx
4244
return Files.createDirectory(tempDir.resolve(other));
4345
}
4446

47+
private Path getRelPathToTop() {
48+
final Path startPath = PathUtils.current().toAbsolutePath();
49+
final Path parent = startPath;
50+
final int nameCount = parent.getNameCount();
51+
final String relName = StringUtils.repeat("../", nameCount);
52+
final Path relPath = Paths.get(relName);
53+
// sanity checks
54+
final Path rootPath = relPath.toAbsolutePath().normalize();
55+
assertNull(rootPath.getFileName());
56+
assertEquals(startPath.getRoot(), rootPath);
57+
return relPath;
58+
}
59+
4560
@Test
4661
void testAbsolutePath(@TempDir final Path fenceRootPath) throws Exception {
4762
// tempDir is the fence
48-
final Path childTest = fenceRootPath.resolve("child/file.txt");
63+
final Path resolved = fenceRootPath.resolve("child/file.txt");
4964
final PathFence fence = PathFence.builder().setRoots(fenceRootPath).get();
5065
// getPath with an absolute string should be allowed
51-
final Path childOk = fence.apply(childTest.toString());
52-
assertEquals(childTest.toAbsolutePath().normalize(), childOk.toAbsolutePath().normalize());
66+
final Path childOk = fence.apply(resolved.toString());
67+
assertEquals(resolved.toAbsolutePath().normalize(), childOk.toAbsolutePath().normalize());
5368
// check with a Path instance should return the same instance when allowed
54-
assertSame(childTest, fence.apply(childTest));
69+
assertSame(resolved, fence.apply(resolved));
5570
}
5671

5772
@ParameterizedTest
@@ -64,6 +79,16 @@ void testEmpty(final String test) {
6479
assertSame(path, fence.apply(path));
6580
}
6681

82+
private void testEscapeAttempt(final Path fenceRootPath) {
83+
final Path resolved = fenceRootPath.resolve("../../etc/passwd");
84+
final Path relative = Paths.get("../../etc/passwd");
85+
final PathFence fence = PathFence.builder().setRoots(fenceRootPath).get();
86+
assertThrows(IllegalArgumentException.class, () -> fence.apply(resolved));
87+
assertThrows(IllegalArgumentException.class, () -> fence.apply(relative));
88+
assertThrows(IllegalArgumentException.class, () -> fence.apply(resolved.toString()));
89+
assertThrows(IllegalArgumentException.class, () -> fence.apply(relative.toString()));
90+
}
91+
6792
@Test
6893
void testMultipleFencePaths(@TempDir final Path tempDir) throws Exception {
6994
// The fence is inside tempDir
@@ -81,10 +106,9 @@ void testMultipleFencePaths(@TempDir final Path tempDir) throws Exception {
81106
@Test
82107
void testNormalization(@TempDir final Path tempDir) throws Exception {
83108
final Path fenceRootPath = createDirectory(tempDir, "root-one");
84-
final Path weird = fenceRootPath.resolve("subdir/../other.txt");
109+
final Path resolved = fenceRootPath.resolve("subdir/../other.txt");
85110
final PathFence fence = PathFence.builder().setRoots(fenceRootPath).get();
86-
// Path contains '..' but after normalization it's still inside the base
87-
assertSame(weird, fence.apply(weird));
111+
assertSame(resolved, fence.apply(resolved));
88112
}
89113

90114
@Test
@@ -98,4 +122,27 @@ void testOutsideFenceThrows(@TempDir final Path tempDir) throws Exception {
98122
assertTrue(msg.contains("not in the fence"), () -> "Expected message to mention fence: " + msg);
99123
assertTrue(msg.contains(other.toAbsolutePath().toString()), () -> "Expected message to contain the path: " + msg);
100124
}
125+
126+
@Test
127+
void testResolveRelative() throws Exception {
128+
final PathFence fence = PathFence.builder().setRoots(Paths.get("/foo/bar")).get();
129+
final Path relPathTop = getRelPathToTop();
130+
final Path relPath = relPathTop.resolve("foo/bar");
131+
assertSame(relPath, fence.apply(relPath));
132+
}
133+
134+
@Test
135+
void testResolveRelativeRoot() throws Exception {
136+
final Path relPathTop = getRelPathToTop();
137+
final PathFence fence = PathFence.builder().setRoots(relPathTop.resolve("foo/bar")).get();
138+
final Path relPath = relPathTop.resolve("foo/bar");
139+
assertSame(relPath, fence.apply(relPath));
140+
}
141+
142+
@ParameterizedTest
143+
@ValueSource(strings = { "/a", "/a/b", "/a/b/c", "/a/b/c/d", "a", "a/b", "a/b/c", "a/b/c/d" })
144+
void testUserHomeDirEscapeAttempt(final String path) throws Exception {
145+
testEscapeAttempt(Paths.get(path));
146+
}
147+
101148
}

0 commit comments

Comments
 (0)