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

coheigea pushed a commit to branch 4.1.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git


The following commit(s) were added to refs/heads/4.1.x-fixes by this push:
     new f7297644ed6 CXF-9223 - Improve prefix matching for OAuth audience URIs 
(#3331)
f7297644ed6 is described below

commit f7297644ed64f6a0a7ee1d8dbb5f80a7b7ee5414
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Thu Jul 23 13:35:03 2026 +0100

    CXF-9223 - Improve prefix matching for OAuth audience URIs (#3331)
    
    (cherry picked from commit 71ec1d3d2f7c6088e651f88038bf724c601a615d)
---
 .../oauth2/filters/OAuthRequestFilter.java         |  26 ++++-
 .../oauth2/filters/OAuthRequestFilterTest.java     | 119 +++++++++++++++++++++
 2 files changed, 144 insertions(+), 1 deletion(-)

diff --git 
a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/filters/OAuthRequestFilter.java
 
b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/filters/OAuthRequestFilter.java
index 0f5d8a04e62..72505c72130 100644
--- 
a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/filters/OAuthRequestFilter.java
+++ 
b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/filters/OAuthRequestFilter.java
@@ -286,7 +286,9 @@ public class OAuthRequestFilter extends 
AbstractAccessTokenValidator
         }
         String requestPath = 
(String)PhaseInterceptorChain.getCurrentMessage().get(Message.REQUEST_URL);
         for (String s : audiences) {
-            boolean matched = completeAudienceMatch ? requestPath.equals(s) : 
requestPath.startsWith(s);
+            // In non-exact mode, only allow prefix matches at 
path/query/fragment boundaries.
+            boolean matched = completeAudienceMatch ? requestPath.equals(s)
+                : matchesAudiencePrefix(requestPath, s);
             if (matched) {
                 return s;
             }
@@ -295,6 +297,28 @@ public class OAuthRequestFilter extends 
AbstractAccessTokenValidator
         return null;
     }
 
+    /**
+     * Checks whether a configured audience matches a request URL using safe 
prefix semantics.
+     * <p>
+     * This keeps subtree-style matching (for example, "/api/read" matching 
"/api/read/item")
+     * but prevents same-prefix sibling matches (for example, 
"/api/readadmin").
+     * A match is accepted only when the configured audience is an exact 
match, ends with '/',
+     * or is followed by a URL boundary character ('/', '?', '#').
+     */
+    protected boolean matchesAudiencePrefix(String requestPath, String 
configuredAudience) {
+        if (requestPath == null || configuredAudience == null) {
+            return false;
+        }
+        if (!requestPath.startsWith(configuredAudience)) {
+            return false;
+        }
+        if (requestPath.length() == configuredAudience.length() || 
configuredAudience.endsWith("/")) {
+            return true;
+        }
+        char boundary = requestPath.charAt(configuredAudience.length());
+        return boundary == '/' || boundary == '?' || boundary == '#';
+    }
+
     public void setCheckFormData(boolean checkFormData) {
         this.checkFormData = checkFormData;
     }
diff --git 
a/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/filters/OAuthRequestFilterTest.java
 
b/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/filters/OAuthRequestFilterTest.java
new file mode 100644
index 00000000000..61da08c5a61
--- /dev/null
+++ 
b/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/filters/OAuthRequestFilterTest.java
@@ -0,0 +1,119 @@
+/**
+ * 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.cxf.rs.security.oauth2.filters;
+
+import java.lang.reflect.Field;
+import java.util.Collections;
+
+import jakarta.ws.rs.NotAuthorizedException;
+import org.apache.cxf.message.Message;
+import org.apache.cxf.message.MessageImpl;
+import org.apache.cxf.phase.PhaseInterceptorChain;
+
+import org.junit.After;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+
+public class OAuthRequestFilterTest {
+
+    @After
+    public void clearCurrentMessage() throws Exception {
+        setThreadLocalMessage(null);
+    }
+
+    @Test
+    public void testValidateAudiencesMatchesSubPathInNonExactMode() throws 
Exception {
+        OAuthRequestFilter filter = new OAuthRequestFilter();
+        filter.setAudienceIsEndpointAddress(true);
+        filter.setCompleteAudienceMatch(false);
+
+        Message message = new MessageImpl();
+        message.put(Message.REQUEST_URL, "/api/read/item");
+        setThreadLocalMessage(message);
+
+        String result = 
filter.validateAudiences(Collections.singletonList("/api/read"));
+        assertEquals("/api/read", result);
+    }
+
+    @Test
+    public void testValidateAudiencesRejectsSiblingPrefixInNonExactMode() 
throws Exception {
+        OAuthRequestFilter filter = new OAuthRequestFilter();
+        filter.setAudienceIsEndpointAddress(true);
+        filter.setCompleteAudienceMatch(false);
+
+        Message message = new MessageImpl();
+        message.put(Message.REQUEST_URL, "/api/readadmin");
+        setThreadLocalMessage(message);
+
+        assertThrows(NotAuthorizedException.class,
+            () -> 
filter.validateAudiences(Collections.singletonList("/api/read")));
+    }
+
+    @Test
+    public void testValidateAudiencesRequiresExactMatchWhenConfigured() throws 
Exception {
+        OAuthRequestFilter filter = new OAuthRequestFilter();
+        filter.setAudienceIsEndpointAddress(true);
+        filter.setCompleteAudienceMatch(true);
+
+        Message message = new MessageImpl();
+        message.put(Message.REQUEST_URL, "/api/read/item");
+        setThreadLocalMessage(message);
+
+        assertThrows(NotAuthorizedException.class,
+            () -> 
filter.validateAudiences(Collections.singletonList("/api/read")));
+    }
+
+    @Test
+    public void testValidateAudiencesSkipsEndpointCheckWhenDisabled() {
+        OAuthRequestFilter filter = new OAuthRequestFilter();
+        filter.setAudienceIsEndpointAddress(false);
+
+        String result = 
filter.validateAudiences(Collections.singletonList("/api/read"));
+        assertNull(result);
+    }
+
+    @Test
+    public void testValidateAudiencesMatchesQueryBoundaryInNonExactMode() 
throws Exception {
+        OAuthRequestFilter filter = new OAuthRequestFilter();
+        filter.setAudienceIsEndpointAddress(true);
+        filter.setCompleteAudienceMatch(false);
+
+        Message message = new MessageImpl();
+        message.put(Message.REQUEST_URL, "/api/read?include=details");
+        setThreadLocalMessage(message);
+
+        String result = 
filter.validateAudiences(Collections.singletonList("/api/read"));
+        assertEquals("/api/read", result);
+    }
+
+    private static void setThreadLocalMessage(Message message) throws 
Exception {
+        Field f = 
PhaseInterceptorChain.class.getDeclaredField("CURRENT_MESSAGE");
+        f.setAccessible(true);
+        @SuppressWarnings("unchecked")
+        ThreadLocal<Message> tl = (ThreadLocal<Message>) f.get(null);
+        if (message == null) {
+            tl.remove();
+        } else {
+            tl.set(message);
+        }
+    }
+}

Reply via email to