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");

Reply via email to