You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@knox.apache.org by pz...@apache.org on 2026/10/12 15:29:03 UTC

(knox) branch master updated: KNOX-3510 - Normalize decoded dispatch URLs and reject path traversal (#1449)

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

pzampino pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/knox.git


The following commit(s) were added to refs/heads/master by this push:
     new 8a55ebe1e KNOX-3510 - Normalize decoded dispatch URLs and reject path traversal (#1449)
8a55ebe1e is described below

commit 8a55ebe1ef5654d708871608c89a3bf39c482f23
Author: Phil Zampino <pz...@apache.org>
AuthorDate: Mon Oct 12 11:28:57 2026 -0400

    KNOX-3510 - Normalize decoded dispatch URLs and reject path traversal (#1449)
    
    Co-authored-by: Claude Code <no...@anthropic.com>
---
 .../knox/gateway/dispatch/URLDecodingDispatch.java | 19 +++++++++++++-
 .../gateway/dispatch/URLDecodingDispatchTest.java  | 30 ++++++++++++++++++++++
 2 files changed, 48 insertions(+), 1 deletion(-)

diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/URLDecodingDispatch.java b/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/URLDecodingDispatch.java
index 34c8f41f7..dd6befb0e 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/URLDecodingDispatch.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/URLDecodingDispatch.java
@@ -52,6 +52,23 @@ public class URLDecodingDispatch extends ConfigurableDispatch {
       str.append('?');
       str.append(query);
     }
-    return URI.create(str.toString());
+    return normalize(URI.create(str.toString()));
+  }
+
+  /**
+   * Decoding can turn encoded dot segments (e.g. %2e%2e%2f) into real ones,
+   * so resolve them and refuse any path that still climbs above its root.
+   */
+  private static URI normalize(final URI uri) {
+    final URI normalized = uri.normalize();
+    final String path = normalized.getPath();
+    if ( path != null ) {
+      for ( final String segment : path.split("/") ) {
+        if ( "..".equals(segment) ) {
+          throw new IllegalArgumentException("Path traversal is not allowed in the dispatch URL");
+        }
+      }
+    }
+    return normalized;
   }
 }
diff --git a/gateway-spi/src/test/java/org/apache/knox/gateway/dispatch/URLDecodingDispatchTest.java b/gateway-spi/src/test/java/org/apache/knox/gateway/dispatch/URLDecodingDispatchTest.java
index c04b6eff2..db6f800da 100644
--- a/gateway-spi/src/test/java/org/apache/knox/gateway/dispatch/URLDecodingDispatchTest.java
+++ b/gateway-spi/src/test/java/org/apache/knox/gateway/dispatch/URLDecodingDispatchTest.java
@@ -58,4 +58,34 @@ public class URLDecodingDispatchTest {
     assertThat(uri.toASCIIString(),
         is("https://localhost:8443/gateway/sandbox/datanode/datanode.html?host=http://localhost:9864"));
   }
+
+  private URI dispatchUrlFor(final String url) {
+    request = EasyMock.createNiceMock(HttpServletRequest.class);
+    EasyMock.expect(request.getRequestURL()).andReturn(new StringBuffer(url)).anyTimes();
+    EasyMock.expect(request.getQueryString()).andReturn(null).anyTimes();
+    EasyMock.replay(request);
+    return dispatch.getDispatchUrl(request);
+  }
+
+  @Test(timeout = TestUtils.MEDIUM_TIMEOUT)
+  public void testEncodedDotSegmentsAreNormalized() {
+    URI uri = dispatchUrlFor("https://localhost:8443/gateway/sandbox/datanode/a/b/%2e%2e/c.html");
+    assertThat(uri.toASCIIString(), is("https://localhost:8443/gateway/sandbox/datanode/a/c.html"));
+
+    uri = dispatchUrlFor("https://localhost:8443/gateway/sandbox/datanode/a/%2e%2e%2fc.html");
+    assertThat(uri.toASCIIString(), is("https://localhost:8443/gateway/sandbox/datanode/c.html"));
+
+    uri = dispatchUrlFor("https://localhost:8443/gateway/sandbox/datanode/./a.html");
+    assertThat(uri.toASCIIString(), is("https://localhost:8443/gateway/sandbox/datanode/a.html"));
+  }
+
+  @Test(timeout = TestUtils.MEDIUM_TIMEOUT, expected = IllegalArgumentException.class)
+  public void testEncodedTraversalAboveRootIsRejected() {
+    dispatchUrlFor("https://localhost:8443/%2e%2e/%2e%2e/etc/passwd");
+  }
+
+  @Test(timeout = TestUtils.MEDIUM_TIMEOUT, expected = IllegalArgumentException.class)
+  public void testTraversalAboveRootIsRejected() {
+    dispatchUrlFor("https://localhost:8443/../etc/passwd");
+  }
 }