This is an automated email from the ASF dual-hosted git repository.
coheigea pushed a commit to branch 3.6.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git
The following commit(s) were added to refs/heads/3.6.x-fixes by this push:
new f39ac4d0290 CXF-9223 - Improve prefix matching for OAuth audience URIs
(#3331)
f39ac4d0290 is described below
commit f39ac4d0290613f85a2433763c905d64df957906
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 6e053aa1022..3bd4df45ce6 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
@@ -279,7 +279,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;
}
@@ -288,6 +290,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..6631ea933c9
--- /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 javax.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);
+ }
+ }
+}