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

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

commit ce091a2fb70e134b4c47ece88be73ba42079b9b8
Author: Sandor Molnar <[email protected]>
AuthorDate: Tue Aug 11 23:24:13 2026 +0200

    KNOX-3414: normalize redirect_uri path before wildcard match (review 
finding M3)
    
    matchesRedirectUri did a startsWith on the un-normalized requested path in 
the
    wildcard branch, so a registration of https://app.example/callback/* matched
    https://app.example/callback/../admin (path "/callback/../admin" starts with
    "/callback") and the authorization code was delivered to /admin -- a 
same-host
    open redirect. The exact, non-wildcard branch was already safe.
    
    Normalize both the base and requested paths (URI.normalize()) before the 
prefix
    compare so "/callback/../admin" collapses to "/admin" and no longer matches 
the
    "/callback" prefix. Genuine sub-paths under the wildcard prefix still match.
    
    Covered by AuthorizeResourceRedirectUriMatchTest.
    
    Co-Authored-By: Claude Opus 4.8 <[email protected]>
---
 .../gateway/service/knoxidf/AuthorizeResource.java | 12 +++-
 .../AuthorizeResourceRedirectUriMatchTest.java     | 74 ++++++++++++++++++++++
 2 files changed, 83 insertions(+), 3 deletions(-)

diff --git 
a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java
 
b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java
index 0ba9ac5bd..9747d3c63 100644
--- 
a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java
+++ 
b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java
@@ -422,7 +422,9 @@ public class AuthorizeResource extends 
PasscodeTokenResourceBase {
         return basicVerificationResponse;
     }
 
-    private boolean matchesRedirectUri(String requestedUri, Set<String> 
registeredUris) {
+    // Package-private for testability (wildcard path-traversal matching is 
exercised by
+    // AuthorizeResourceRedirectUriMatchTest); not part of the public resource 
API.
+    boolean matchesRedirectUri(String requestedUri, Set<String> 
registeredUris) {
         final URI requested = parseUri(requestedUri);
         if (requested == null) {
             return false;
@@ -434,8 +436,12 @@ public class AuthorizeResource extends 
PasscodeTokenResourceBase {
                 // "https://good.example*"; match 
"https://good.example.evil.com";.
                 final URI base = parseUri(registered.substring(0, 
registered.length() - 1));
                 if (base != null && sameOrigin(base, requested)) {
-                    final String basePath = base.getPath() == null ? "" : 
base.getPath();
-                    final String reqPath = requested.getPath() == null ? "" : 
requested.getPath();
+                    // Normalize the requested path before the prefix compare 
so a traversal segment
+                    // cannot escape the registered prefix: a raw startsWith 
would let
+                    // ".../callback/../admin" match ".../callback/*" and 
deliver the code to /admin.
+                    // normalize() collapses "/callback/../admin" to "/admin", 
which no longer matches.
+                    final String basePath = base.normalize().getPath() == null 
? "" : base.normalize().getPath();
+                    final String reqPath = requested.normalize().getPath() == 
null ? "" : requested.normalize().getPath();
                     if (reqPath.startsWith(basePath)) {
                         return true;
                     }
diff --git 
a/gateway-service-knoxidf/src/test/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResourceRedirectUriMatchTest.java
 
b/gateway-service-knoxidf/src/test/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResourceRedirectUriMatchTest.java
new file mode 100644
index 000000000..01c88eaf9
--- /dev/null
+++ 
b/gateway-service-knoxidf/src/test/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResourceRedirectUriMatchTest.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations 
under
+ * the License.
+ */
+package org.apache.knox.gateway.service.knoxidf;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Collections;
+import java.util.Set;
+
+import org.junit.Test;
+
+/**
+ * Verifies wildcard redirect_uri matching rejects path traversal (review 
finding M3). A wildcard
+ * registration such as {@code https://app.example/callback/*} must not match
+ * {@code https://app.example/callback/../admin}: a raw startsWith on the 
un-normalized path would
+ * let the traversal escape the registered prefix and deliver the 
authorization code to /admin
+ * (a same-host open redirect). The path is normalized before the prefix 
compare.
+ */
+public class AuthorizeResourceRedirectUriMatchTest {
+
+  private final AuthorizeResource resource = new AuthorizeResource();
+
+  private boolean matches(final String requested, final String registered) {
+    final Set<String> registeredUris = Collections.singleton(registered);
+    return resource.matchesRedirectUri(requested, registeredUris);
+  }
+
+  @Test
+  public void testTraversalEscapingWildcardPrefixIsRejected() {
+    assertFalse("A traversal that resolves outside the registered prefix must 
be rejected.",
+        matches("https://app.example/callback/../admin";, 
"https://app.example/callback/*";));
+  }
+
+  @Test
+  public void testEncodedPrefixSuffixStillMatches() {
+    assertTrue("A genuine path under the wildcard prefix must still match.",
+        matches("https://app.example/callback/oauth";, 
"https://app.example/callback/*";));
+  }
+
+  @Test
+  public void testExactPrefixMatchesWildcard() {
+    assertTrue("The wildcard base path itself must match.",
+        matches("https://app.example/callback/";, 
"https://app.example/callback/*";));
+  }
+
+  @Test
+  public void testDifferentOriginIsRejected() {
+    assertFalse("A same-prefix path on a different host must be rejected.",
+        matches("https://app.example.evil.com/callback/x";, 
"https://app.example/callback/*";));
+  }
+
+  @Test
+  public void testExact(){
+    assertTrue("An exact non-wildcard registration must match verbatim.",
+        matches("https://app.example/cb";, "https://app.example/cb";));
+    assertFalse("An exact registration must not match a different path.",
+        matches("https://app.example/cb2";, "https://app.example/cb";));
+  }
+}

Reply via email to