This is an automated email from the ASF dual-hosted git repository. joerghoh pushed a commit to branch SLING-13373 in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-api.git
commit bb1b3e4a7a0b74fda80088e838a0a0bc835927c0 Author: Joerg Hoh <[email protected]> AuthorDate: Thu Oct 1 20:52:52 2026 +0200 SLING-13373 Path.matches() should respect ../ segments --- .../org/apache/sling/api/resource/path/Path.java | 18 +++++++--- .../apache/sling/api/resource/path/PathSet.java | 3 ++ .../sling/api/resource/path/PathSetTest.java | 23 +++++++++++++ .../apache/sling/api/resource/path/PathTest.java | 40 ++++++++++++++++++++++ 4 files changed, 80 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/apache/sling/api/resource/path/Path.java b/src/main/java/org/apache/sling/api/resource/path/Path.java index 77326aa..6f1c7ad 100644 --- a/src/main/java/org/apache/sling/api/resource/path/Path.java +++ b/src/main/java/org/apache/sling/api/resource/path/Path.java @@ -20,6 +20,7 @@ package org.apache.sling.api.resource.path; import java.util.regex.Pattern; +import org.apache.sling.api.resource.ResourceUtil; import org.jetbrains.annotations.NotNull; /** @@ -102,8 +103,13 @@ public class Path implements Comparable<Path> { * provided path matches the pattern. If this path object holds a pattern * and a pattern is provided as the argument, it returns only {@code true} * if the pattern is the same. - * If the provided argument is not an absolute path (e.g. if it is a relative - * path or a pattern), this method returns {@code false}. + * If the provided argument is a concrete path, it is normalized using + * {@link ResourceUtil#normalize(String)} before matching, resolving {@code .} + * and {@code ..} segments and collapsing consecutive slashes. + * If normalization fails, this method returns {@code false}. + * Glob pattern arguments retain their pattern semantics. + * If the provided argument is not an absolute path or an absolute glob + * pattern, this method throws {@code IllegalArgumentException}. * * @param otherPath Absolute path to check. * @return {@code true} If other path is within the sub tree of this path @@ -153,10 +159,14 @@ public class Path implements Comparable<Path> { if (!otherPath.startsWith("/")) { throw new IllegalArgumentException("Path must be absolute: " + otherPath); } + final String normalizedOtherPath = ResourceUtil.normalize(otherPath); + if (normalizedOtherPath == null) { + return false; + } if (isPattern) { - return this.regexPattern.matcher(otherPath).matches(); + return this.regexPattern.matcher(normalizedOtherPath).matches(); } - return this.path.equals(otherPath) || otherPath.startsWith(this.prefix); + return this.path.equals(normalizedOtherPath) || normalizedOtherPath.startsWith(this.prefix); } /** diff --git a/src/main/java/org/apache/sling/api/resource/path/PathSet.java b/src/main/java/org/apache/sling/api/resource/path/PathSet.java index 3bb6324..373cb28 100644 --- a/src/main/java/org/apache/sling/api/resource/path/PathSet.java +++ b/src/main/java/org/apache/sling/api/resource/path/PathSet.java @@ -125,6 +125,9 @@ public class PathSet implements Iterable<Path> { /** * Check whether the provided path is in the sub tree of any * of the paths in this set. + * Concrete path arguments are normalized before matching; paths that + * cannot be normalized do not match. Glob pattern arguments retain their + * pattern semantics. * @param otherPath The path to match * @return The path which matches the provided path, {@code null} otherwise. * @see Path#matches(String) diff --git a/src/test/java/org/apache/sling/api/resource/path/PathSetTest.java b/src/test/java/org/apache/sling/api/resource/path/PathSetTest.java index 734eadd..cdc2b1c 100644 --- a/src/test/java/org/apache/sling/api/resource/path/PathSetTest.java +++ b/src/test/java/org/apache/sling/api/resource/path/PathSetTest.java @@ -113,6 +113,29 @@ public class PathSetTest { assertEquals(new Path("/x/y"), set.matches("/x/y/g/e")); } + @Test + public void testMatchingNormalizesTraversalSegments() { + for (final String entry : new String[] {"/apps", "glob:/apps/**"}) { + final PathSet set = PathSet.fromStrings(entry); + + assertNull(set.matches("/apps/../etc/secret")); + assertNull(set.matches("/apps/foo/../../etc")); + assertNull(set.matches("/../apps/foo")); + assertNull(set.matches("/apps/...")); + assertEquals(new Path(entry), set.matches("/apps/foo/../bar")); + assertEquals(new Path(entry), set.matches("/libs/../apps/foo")); + assertEquals(new Path(entry), set.matches("/apps//foo")); + } + } + + @Test + public void testSubsetNormalizesTraversalSegments() { + final PathSet set = PathSet.fromStrings("/apps/../etc/secret", "/apps/foo"); + + assertEqualSets(set.getSubset("/apps"), "/apps/foo"); + assertEqualSets(set.getSubset(PathSet.fromStrings("/apps")), "/apps/foo"); + } + @Test public void testToStringSet() { final PathSet set = PathSet.fromStrings("/a", "/x/y"); diff --git a/src/test/java/org/apache/sling/api/resource/path/PathTest.java b/src/test/java/org/apache/sling/api/resource/path/PathTest.java index 0450693..abf7859 100644 --- a/src/test/java/org/apache/sling/api/resource/path/PathTest.java +++ b/src/test/java/org/apache/sling/api/resource/path/PathTest.java @@ -63,6 +63,46 @@ public class PathTest { assertMatch(p, "/content", "/content/a", "/content/a/b"); } + @Test + public void testMatchesNormalizesTraversalSegments() { + final Path path = new Path("/apps"); + + assertNoMatch(path, "/apps/../etc/secret", "/apps/./../etc/secret", "/apps/foo/../../etc"); + assertMatch( + path, + "/apps/foo/../bar", + "/libs/../apps/foo", + "/apps//foo", + "/apps/foo/.", + "/apps/foo/..", + "/apps/.hidden", + "/apps/foo..bar"); + assertNoMatch(path, "/../apps/foo", "/apps/../../..", "/apps/...", "/apps/..../foo"); + } + + @Test + public void testPatternMatchNormalizesTraversalSegments() { + final Path glob = new Path("glob:/apps/**"); + + assertNoMatch(glob, "/apps/../etc/secret", "/apps/foo/../../etc", "/../apps/foo", "/apps/..."); + assertMatch(glob, "/apps/foo/../bar", "/libs/../apps/foo", "/apps//foo"); + + final Path singleSegmentGlob = new Path("glob:/apps/*"); + assertMatch(singleSegmentGlob, "/apps/foo/../bar", "/apps//bar"); + assertNoMatch(singleSegmentGlob, "/apps/foo/bar", "/apps/../etc"); + } + + @Test + public void testRootMatchRejectsInvalidPaths() { + final Path root = new Path("/"); + final Path glob = new Path("glob:/**"); + + assertNoMatch(root, "/../apps", "/apps/../../etc", "/apps/..."); + assertNoMatch(glob, "/../apps", "/apps/../../etc", "/apps/..."); + assertMatch(root, "/apps/..", "//apps//foo", "/apps/."); + assertMatch(glob, "/apps/..", "//apps//foo", "/apps/."); + } + @Test public void testPatternMatchingA() { final Path p = new Path("glob:/apps/**/*.html");
