This is an automated email from the ASF dual-hosted git repository.
coheigea pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cxf.git
The following commit(s) were added to refs/heads/main by this push:
new 4861d6922a5 For OIDC, allow only relative URLs or absolute URLs with
the same scheme and authority as the current RP (#3398)
4861d6922a5 is described below
commit 4861d6922a570aa7e02a48f163245eb94790ed61
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Wed Aug 26 09:20:00 2026 +0100
For OIDC, allow only relative URLs or absolute URLs with the same scheme
and authority as the current RP (#3398)
---
.../oidc/rp/OidcRpAuthenticationFilter.java | 5 +-
.../oidc/rp/OidcRpAuthenticationService.java | 16 ++-
.../oidc/rp/OidcRpAuthenticationUtils.java | 36 +++++++
.../oidc/rp/OidcRpAuthenticationServiceTest.java | 120 +++++++++++++++++++++
4 files changed, 172 insertions(+), 5 deletions(-)
diff --git
a/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/OidcRpAuthenticationFilter.java
b/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/OidcRpAuthenticationFilter.java
index b08d907fe64..e835252f81b 100644
---
a/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/OidcRpAuthenticationFilter.java
+++
b/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/OidcRpAuthenticationFilter.java
@@ -137,10 +137,7 @@ public class OidcRpAuthenticationFilter implements
ContainerRequestFilter {
return true;
}
URI base = rc.getUriInfo().getAbsolutePath();
- return uri.getScheme() != null
- && uri.getScheme().equalsIgnoreCase(base.getScheme())
- && uri.getAuthority() != null
- && uri.getAuthority().equalsIgnoreCase(base.getAuthority());
+ return OidcRpAuthenticationUtils.isSameOrigin(base, uri);
}
public void setRedirectUri(String redirectUri) {
this.redirectUri = redirectUri;
diff --git
a/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/OidcRpAuthenticationService.java
b/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/OidcRpAuthenticationService.java
index 30d957bf6a2..892278340d2 100644
---
a/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/OidcRpAuthenticationService.java
+++
b/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/OidcRpAuthenticationService.java
@@ -61,7 +61,7 @@ public class OidcRpAuthenticationService {
String basePath = (String)mc.get("http.base.path");
redirectUri =
UriBuilder.fromUri(basePath).path(defaultLocation).build();
} else if (location != null) {
- redirectUri = URI.create(UrlUtils.urlDecode(location));
+ redirectUri = getSameOriginUri(location);
}
if (redirectUri != null) {
return Response.seeOther(redirectUri).build();
@@ -69,6 +69,20 @@ public class OidcRpAuthenticationService {
return Response.ok(oidcContext).build();
}
+ private URI getSameOriginUri(String location) {
+ final URI uri;
+ try {
+ uri = URI.create(UrlUtils.urlDecode(location));
+ } catch (IllegalArgumentException ex) {
+ return null;
+ }
+ URI base = mc.getUriInfo().getAbsolutePath();
+ if (OidcRpAuthenticationUtils.isSameOrigin(base, uri)) {
+ return uri;
+ }
+ return null;
+ }
+
public void setDefaultLocation(String defaultLocation) {
this.defaultLocation = defaultLocation;
}
diff --git
a/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/OidcRpAuthenticationUtils.java
b/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/OidcRpAuthenticationUtils.java
new file mode 100644
index 00000000000..46f18dcc74a
--- /dev/null
+++
b/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/OidcRpAuthenticationUtils.java
@@ -0,0 +1,36 @@
+/**
+ * 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.oidc.rp;
+
+import java.net.URI;
+
+final class OidcRpAuthenticationUtils {
+ private OidcRpAuthenticationUtils() {
+ }
+
+ static boolean isSameOrigin(URI base, URI uri) {
+ if (uri.getScheme() == null && uri.getAuthority() == null) {
+ return true;
+ }
+ return uri.getScheme() != null
+ && uri.getScheme().equalsIgnoreCase(base.getScheme())
+ && uri.getAuthority() != null
+ && uri.getAuthority().equalsIgnoreCase(base.getAuthority());
+ }
+}
diff --git
a/rt/rs/security/sso/oidc/src/test/java/org/apache/cxf/rs/security/oidc/rp/OidcRpAuthenticationServiceTest.java
b/rt/rs/security/sso/oidc/src/test/java/org/apache/cxf/rs/security/oidc/rp/OidcRpAuthenticationServiceTest.java
new file mode 100644
index 00000000000..2a17b10f53b
--- /dev/null
+++
b/rt/rs/security/sso/oidc/src/test/java/org/apache/cxf/rs/security/oidc/rp/OidcRpAuthenticationServiceTest.java
@@ -0,0 +1,120 @@
+/**
+ * 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.oidc.rp;
+
+import java.lang.reflect.Field;
+import java.net.URI;
+
+import jakarta.ws.rs.core.MultivaluedHashMap;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.UriInfo;
+import org.apache.cxf.common.util.UrlUtils;
+import org.apache.cxf.jaxrs.ext.MessageContext;
+import org.apache.cxf.jaxrs.ext.MessageContextImpl;
+import org.apache.cxf.message.ExchangeImpl;
+import org.apache.cxf.message.MessageImpl;
+import org.apache.cxf.rs.security.oauth2.client.ClientTokenContextManager;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class OidcRpAuthenticationServiceTest {
+ private static final URI REQUEST_URI =
URI.create("https://app.example.com:8080/services/rp/complete");
+
+ @Test
+ public void testRejectsCrossOriginRedirect() {
+ Response response =
completeWithState("https://evil.example.com/phish");
+
+ assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+ assertNull(response.getHeaderString("Location"));
+ }
+
+ @Test
+ public void testRejectsProtocolRelativeRedirect() {
+ Response response = completeWithState("//evil.example.com/phish");
+
+ assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+ assertNull(response.getHeaderString("Location"));
+ }
+
+ @Test
+ public void testRejectsDoubleEncodedCrossOriginRedirect() {
+ String attackerLocation =
"https%253A%252F%252Fevil.example.com%252Fphish";
+ String callbackLocation = UrlUtils.urlDecode(attackerLocation);
+ Response response = completeWithState(callbackLocation);
+
+ assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+ assertNull(response.getHeaderString("Location"));
+ }
+
+ @Test
+ public void testAllowsSameOriginAbsoluteRedirect() {
+ Response response =
completeWithState("https://app.example.com:8080/services/protected");
+
+ assertEquals(Response.Status.SEE_OTHER.getStatusCode(),
response.getStatus());
+ assertEquals("https://app.example.com:8080/services/protected",
+ response.getHeaderString("Location"));
+ }
+
+ @Test
+ public void testAllowsRelativeRedirect() {
+ Response response = completeWithState("/services/protected");
+
+ assertEquals(Response.Status.SEE_OTHER.getStatusCode(),
response.getStatus());
+ assertEquals("/services/protected",
response.getHeaderString("Location"));
+ }
+
+ private Response completeWithState(String location) {
+ OidcClientTokenContext context = new OidcClientTokenContextImpl();
+ MultivaluedHashMap<String, String> state = new MultivaluedHashMap<>();
+ state.putSingle("state", location);
+ ((OidcClientTokenContextImpl)context).setState(state);
+
+ UriInfo uriInfo = mock(UriInfo.class);
+ when(uriInfo.getAbsolutePath()).thenReturn(REQUEST_URI);
+ MessageImpl message = new MessageImpl();
+ message.setExchange(new ExchangeImpl());
+ MessageContext messageContext = new MessageContextImpl(message) {
+ @Override
+ public UriInfo getUriInfo() {
+ return uriInfo;
+ }
+ };
+
+ OidcRpAuthenticationService service = new
OidcRpAuthenticationService();
+
service.setClientTokenContextManager(mock(ClientTokenContextManager.class));
+ setMessageContext(service, messageContext);
+ return service.completeAuthentication(context);
+ }
+
+ private void setMessageContext(OidcRpAuthenticationService service,
+ org.apache.cxf.jaxrs.ext.MessageContext
messageContext) {
+ try {
+ Field field =
OidcRpAuthenticationService.class.getDeclaredField("mc");
+ field.setAccessible(true);
+ field.set(service, messageContext);
+ } catch (ReflectiveOperationException ex) {
+ throw new IllegalStateException(ex);
+ }
+ }
+}