Skip to content

Commit 69b9e0a

Browse files
committed
Address relative path issues
1 parent 2692adf commit 69b9e0a

2 files changed

Lines changed: 71 additions & 15 deletions

File tree

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: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.nio.file.Path;
2929
import java.nio.file.Paths;
3030

31+
import org.apache.commons.lang3.StringUtils;
3132
import org.junit.jupiter.api.Test;
3233
import org.junit.jupiter.api.io.TempDir;
3334
import org.junit.jupiter.params.ParameterizedTest;
@@ -42,16 +43,29 @@ private Path createDirectory(final Path tempDir, final String other) throws IOEx
4243
return Files.createDirectory(tempDir.resolve(other));
4344
}
4445

46+
private Path getRelPathToTop() {
47+
final Path startPath = PathUtils.current().toAbsolutePath();
48+
final Path parent = startPath;
49+
final int nameCount = parent.getNameCount();
50+
final String relName = StringUtils.repeat("../", nameCount);
51+
final Path relPath = Paths.get(relName);
52+
// sanity checks
53+
final Path rootPath = relPath.toAbsolutePath().normalize();
54+
assertEquals("/", rootPath.toString());
55+
assertEquals(startPath.getRoot(), rootPath);
56+
return relPath;
57+
}
58+
4559
@Test
4660
void testAbsolutePath(@TempDir final Path fenceRootPath) throws Exception {
4761
// tempDir is the fence
48-
final Path childTest = fenceRootPath.resolve("child/file.txt");
62+
final Path resolved = fenceRootPath.resolve("child/file.txt");
4963
final PathFence fence = PathFence.builder().setRoots(fenceRootPath).get();
5064
// getPath with an absolute string should be allowed
51-
final Path childOk = fence.apply(childTest.toString());
52-
assertEquals(childTest.toAbsolutePath().normalize(), childOk.toAbsolutePath().normalize());
65+
final Path childOk = fence.apply(resolved.toString());
66+
assertEquals(resolved.toAbsolutePath().normalize(), childOk.toAbsolutePath().normalize());
5367
// check with a Path instance should return the same instance when allowed
54-
assertSame(childTest, fence.apply(childTest));
68+
assertSame(resolved, fence.apply(resolved));
5569
}
5670

5771
@ParameterizedTest
@@ -64,6 +78,16 @@ void testEmpty(final String test) {
6478
assertSame(path, fence.apply(path));
6579
}
6680

81+
private void testEscapeAttempt(final Path fenceRootPath) {
82+
final Path resolved = fenceRootPath.resolve("../../etc/passwd");
83+
final Path relative = Paths.get("../../etc/passwd");
84+
final PathFence fence = PathFence.builder().setRoots(fenceRootPath).get();
85+
assertThrows(IllegalArgumentException.class, () -> fence.apply(resolved));
86+
assertThrows(IllegalArgumentException.class, () -> fence.apply(relative));
87+
assertThrows(IllegalArgumentException.class, () -> fence.apply(resolved.toString()));
88+
assertThrows(IllegalArgumentException.class, () -> fence.apply(relative.toString()));
89+
}
90+
6791
@Test
6892
void testMultipleFencePaths(@TempDir final Path tempDir) throws Exception {
6993
// The fence is inside tempDir
@@ -81,10 +105,9 @@ void testMultipleFencePaths(@TempDir final Path tempDir) throws Exception {
81105
@Test
82106
void testNormalization(@TempDir final Path tempDir) throws Exception {
83107
final Path fenceRootPath = createDirectory(tempDir, "root-one");
84-
final Path weird = fenceRootPath.resolve("subdir/../other.txt");
108+
final Path resolved = fenceRootPath.resolve("subdir/../other.txt");
85109
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));
110+
assertSame(resolved, fence.apply(resolved));
88111
}
89112

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

0 commit comments

Comments
 (0)