This is an automated email from the ASF dual-hosted git repository.

mimaison pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/kafka.git


The following commit(s) were added to refs/heads/trunk by this push:
     new d995cd14cca KAFKA-20700: Resolve symlinks in AllowedPaths before 
validation (#22631)
d995cd14cca is described below

commit d995cd14ccad3b4f1d4e8dfb80634d97d594a947
Author: nileshkumar3 <[email protected]>
AuthorDate: Wed Jun 24 13:18:16 2026 -0500

    KAFKA-20700: Resolve symlinks in AllowedPaths before validation (#22631)
    
    Resolve symlinks with toRealPath() in AllowedPaths before
    validating paths against allowed.paths, so symlinks inside an allowed
    directory cannot point outside the boundary.
    
    Reviewers: Mickael Maison <[email protected]>
---
 .../common/config/internals/AllowedPaths.java      | 19 +++++++++++-----
 .../common/config/provider/AllowedPathsTest.java   | 26 ++++++++++++++++++----
 2 files changed, 36 insertions(+), 9 deletions(-)

diff --git 
a/clients/src/main/java/org/apache/kafka/common/config/internals/AllowedPaths.java
 
b/clients/src/main/java/org/apache/kafka/common/config/internals/AllowedPaths.java
index b0f1f1a29c7..300772ee683 100644
--- 
a/clients/src/main/java/org/apache/kafka/common/config/internals/AllowedPaths.java
+++ 
b/clients/src/main/java/org/apache/kafka/common/config/internals/AllowedPaths.java
@@ -18,6 +18,7 @@ package org.apache.kafka.common.config.internals;
 
 import org.apache.kafka.common.config.ConfigException;
 
+import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -49,7 +50,11 @@ public class AllowedPaths {
                 } else if (!Files.exists(normalisedPath)) {
                     throw new ConfigException("Path " + normalisedPath + " 
does not exist");
                 } else {
-                    allowedPaths.add(normalisedPath);
+                    try {
+                        allowedPaths.add(normalisedPath.toRealPath());
+                    } catch (IOException e) {
+                        throw new ConfigException("Path " + normalisedPath + " 
could not be resolved", e);
+                    }
                 }
             });
 
@@ -69,12 +74,16 @@ public class AllowedPaths {
         Path parsedPath = Paths.get(path);
 
         if (allowedPaths != null) {
-            Path normalisedPath = parsedPath.normalize();
-            long allowed = 
allowedPaths.stream().filter(normalisedPath::startsWith).count();
-            if (allowed == 0) {
+            try {
+                Path realPath = parsedPath.toRealPath();
+                long allowed = 
allowedPaths.stream().filter(realPath::startsWith).count();
+                if (allowed == 0) {
+                    return null;
+                }
+                return realPath;
+            } catch (IOException e) {
                 return null;
             }
-            return normalisedPath;
         }
 
         return parsedPath;
diff --git 
a/clients/src/test/java/org/apache/kafka/common/config/provider/AllowedPathsTest.java
 
b/clients/src/test/java/org/apache/kafka/common/config/provider/AllowedPathsTest.java
index ebcaf63f880..29bc425a059 100644
--- 
a/clients/src/test/java/org/apache/kafka/common/config/provider/AllowedPathsTest.java
+++ 
b/clients/src/test/java/org/apache/kafka/common/config/provider/AllowedPathsTest.java
@@ -50,11 +50,11 @@ class AllowedPathsTest {
     }
 
     @Test
-    public void testAllowedPath() {
+    public void testAllowedPath() throws IOException {
         allowedPaths = new AllowedPaths(String.join(",", dir, dir2));
 
         Path actual = allowedPaths.parseUntrustedPath(myFile);
-        assertEquals(myFile, actual.toString());
+        assertEquals(Paths.get(myFile).toRealPath(), actual);
     }
 
     @Test
@@ -82,12 +82,30 @@ class AllowedPathsTest {
     }
 
     @Test
-    public void testAllowedTraversal() {
+    public void testAllowedTraversal() throws IOException {
         allowedPaths = new AllowedPaths(String.join(",", dir, dir2));
 
         Path traversedPath = Paths.get(dir, "..", "dir2");
         Path actual = 
allowedPaths.parseUntrustedPath(traversedPath.toString());
-        assertEquals(traversedPath.normalize(), actual);
+        assertEquals(Paths.get(dir2).toRealPath(), actual);
+    }
+
+    @Test
+    public void testSymlinkOutsideAllowedDirRejected() throws IOException {
+        Path outsideFile = Files.createFile(Paths.get(dir2, "outsideFile"));
+        Path symlink = Files.createSymbolicLink(Paths.get(dir, "link"), 
outsideFile);
+
+        allowedPaths = new AllowedPaths(dir);
+        assertNull(allowedPaths.parseUntrustedPath(symlink.toString()));
+    }
+
+    @Test
+    public void testSymlinkInsideAllowedDirAllowed() throws IOException {
+        Path symlink = Files.createSymbolicLink(Paths.get(dir, "link"), 
Paths.get(myFile));
+
+        allowedPaths = new AllowedPaths(dir);
+        Path actual = allowedPaths.parseUntrustedPath(symlink.toString());
+        assertEquals(Paths.get(myFile).toRealPath(), actual);
     }
 
     @Test

Reply via email to