From c212f79b8e286f4ed04cd323aab0439f70fa3bd6 Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Mon, 20 Jul 2026 14:08:06 +0200 Subject: [PATCH 1/6] WW-5604 Add CdiProxyService to detect Weld client proxies Co-Authored-By: Claude Opus 4.8 --- plugins/cdi/pom.xml | 7 ++ .../apache/struts2/cdi/CdiProxyService.java | 93 +++++++++++++++++++ .../struts2/cdi/CdiProxyServiceTest.java | 88 ++++++++++++++++++ .../apache/struts2/cdi/ProxiedFooService.java | 33 +++++++ 4 files changed, 221 insertions(+) create mode 100644 plugins/cdi/src/main/java/org/apache/struts2/cdi/CdiProxyService.java create mode 100644 plugins/cdi/src/test/java/org/apache/struts2/cdi/CdiProxyServiceTest.java create mode 100644 plugins/cdi/src/test/java/org/apache/struts2/cdi/ProxiedFooService.java diff --git a/plugins/cdi/pom.xml b/plugins/cdi/pom.xml index 12a39b59cf..696f976492 100644 --- a/plugins/cdi/pom.xml +++ b/plugins/cdi/pom.xml @@ -39,6 +39,13 @@ provided + + org.jboss.weld + weld-api + 6.0.Final + provided + + org.jboss.weld weld-core-impl diff --git a/plugins/cdi/src/main/java/org/apache/struts2/cdi/CdiProxyService.java b/plugins/cdi/src/main/java/org/apache/struts2/cdi/CdiProxyService.java new file mode 100644 index 0000000000..637287bb86 --- /dev/null +++ b/plugins/cdi/src/main/java/org/apache/struts2/cdi/CdiProxyService.java @@ -0,0 +1,93 @@ +/* + * 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.struts2.cdi; + +import org.apache.commons.lang3.reflect.MethodUtils; +import org.apache.struts2.inject.Inject; +import org.apache.struts2.ognl.ProxyCacheFactory; +import org.apache.struts2.util.StrutsProxyService; +import org.jboss.weld.proxy.WeldClientProxy; + +import java.lang.reflect.Member; +import java.lang.reflect.Method; + +/** + * CDI-aware {@link org.apache.struts2.util.ProxyService}. Extends the default + * {@link StrutsProxyService} (Spring + Hibernate detection) with recognition of + * Weld client proxies, so {@code SecurityMemberAccess} can resolve the real + * target class of a normal-scoped CDI bean before evaluating the OGNL allowlist. + * + * @see WW-5604 + */ +public class CdiProxyService extends StrutsProxyService { + + @Inject + public CdiProxyService(ProxyCacheFactory proxyCacheFactory) { + super(proxyCacheFactory); + } + + @Override + public boolean isProxy(Object object) { + return super.isProxy(object) || isWeldProxy(object); + } + + @Override + public boolean isProxyMember(Member member, Object object) { + return super.isProxyMember(member, object) || isWeldProxyMember(member); + } + + @Override + public Class ultimateTargetClass(Object candidate) { + if (isWeldProxy(candidate)) { + return weldUltimateTargetClass(candidate); + } + return super.ultimateTargetClass(candidate); + } + + private boolean isWeldProxy(Object object) { + if (object == null) { + return false; + } + try { + return object instanceof WeldClientProxy; + } catch (LinkageError ignored) { + return false; + } + } + + private Class weldUltimateTargetClass(Object candidate) { + try { + Object instance = ((WeldClientProxy) candidate).getMetadata().getContextualInstance(); + return instance != null ? instance.getClass() : candidate.getClass(); + } catch (LinkageError ignored) { + return candidate.getClass(); + } + } + + private boolean isWeldProxyMember(Member member) { + try { + if (member instanceof Method method) { + return MethodUtils.getMatchingMethod(WeldClientProxy.class, member.getName(), method.getParameterTypes()) != null; + } + return false; + } catch (LinkageError ignored) { + return false; + } + } +} diff --git a/plugins/cdi/src/test/java/org/apache/struts2/cdi/CdiProxyServiceTest.java b/plugins/cdi/src/test/java/org/apache/struts2/cdi/CdiProxyServiceTest.java new file mode 100644 index 0000000000..92013c823e --- /dev/null +++ b/plugins/cdi/src/test/java/org/apache/struts2/cdi/CdiProxyServiceTest.java @@ -0,0 +1,88 @@ +/* + * 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.struts2.cdi; + +import org.apache.struts2.ognl.StrutsProxyCacheFactory; +import org.apache.struts2.util.ProxyService; +import org.jboss.weld.bootstrap.api.helpers.RegistrySingletonProvider; +import org.jboss.weld.environment.se.Weld; +import org.jboss.weld.environment.se.WeldContainer; +import org.jboss.weld.proxy.WeldClientProxy; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.lang.reflect.Method; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CdiProxyServiceTest { + + private static WeldContainer container; + + private ProxyService proxyService; + private ProxiedFooService proxy; + + @BeforeClass + public static void startContainer() { + container = new Weld().containerId(RegistrySingletonProvider.STATIC_INSTANCE).initialize(); + } + + @AfterClass + public static void stopContainer() { + container.shutdown(); + } + + @Before + public void setUp() { + proxyService = new CdiProxyService(new StrutsProxyCacheFactory<>("1000", "basic")); + proxy = container.select(ProxiedFooService.class).get(); + } + + @Test + public void weldClientProxyIsRecognisedAsProxy() { + assertThat(proxy).isInstanceOf(WeldClientProxy.class); // sanity: Weld really produced a proxy + assertThat(proxyService.isProxy(proxy)).isTrue(); + } + + @Test + public void ultimateTargetClassResolvesRealClass() { + assertThat(proxyService.ultimateTargetClass(proxy)).isEqualTo(ProxiedFooService.class); + } + + @Test + public void weldProxyAccessorIsProxyMember() throws Exception { + Method getMetadata = proxy.getClass().getMethod("getMetadata"); + assertThat(proxyService.isProxyMember(getMetadata, proxy)).isTrue(); + } + + @Test + public void realBeanMethodIsNotProxyMember() throws Exception { + Method getHello = proxy.getClass().getMethod("getHello"); + assertThat(proxyService.isProxyMember(getHello, proxy)).isFalse(); + } + + @Test + public void plainObjectIsNotProxy() { + Object plain = new Object(); + assertThat(proxyService.isProxy(plain)).isFalse(); + assertThat(proxyService.ultimateTargetClass(plain)).isEqualTo(Object.class); + } +} diff --git a/plugins/cdi/src/test/java/org/apache/struts2/cdi/ProxiedFooService.java b/plugins/cdi/src/test/java/org/apache/struts2/cdi/ProxiedFooService.java new file mode 100644 index 0000000000..45b824d82b --- /dev/null +++ b/plugins/cdi/src/test/java/org/apache/struts2/cdi/ProxiedFooService.java @@ -0,0 +1,33 @@ +/* + * 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.struts2.cdi; + +import jakarta.enterprise.context.ApplicationScoped; + +/** + * A normal-scoped CDI bean. Weld serves it through a client proxy that + * implements {@link org.jboss.weld.proxy.WeldClientProxy}. + */ +@ApplicationScoped +public class ProxiedFooService { + + public String getHello() { + return "Hello"; + } +} From cf766825f1665db2cdc6fd56723d6d2cf0bc6508 Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Mon, 20 Jul 2026 14:14:23 +0200 Subject: [PATCH 2/6] WW-5604 Register CdiProxyService as the active ProxyService Co-Authored-By: Claude Opus 4.8 --- .../cdi/src/main/resources/struts-plugin.xml | 3 + .../cdi/CdiSecurityMemberAccessProxyTest.java | 88 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 plugins/cdi/src/test/java/org/apache/struts2/cdi/CdiSecurityMemberAccessProxyTest.java diff --git a/plugins/cdi/src/main/resources/struts-plugin.xml b/plugins/cdi/src/main/resources/struts-plugin.xml index b0458e9409..c69b2073fb 100644 --- a/plugins/cdi/src/main/resources/struts-plugin.xml +++ b/plugins/cdi/src/main/resources/struts-plugin.xml @@ -30,4 +30,7 @@ + + + diff --git a/plugins/cdi/src/test/java/org/apache/struts2/cdi/CdiSecurityMemberAccessProxyTest.java b/plugins/cdi/src/test/java/org/apache/struts2/cdi/CdiSecurityMemberAccessProxyTest.java new file mode 100644 index 0000000000..6d6a74914e --- /dev/null +++ b/plugins/cdi/src/test/java/org/apache/struts2/cdi/CdiSecurityMemberAccessProxyTest.java @@ -0,0 +1,88 @@ +/* + * 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.struts2.cdi; + +import ognl.Ognl; +import ognl.OgnlContext; +import org.apache.struts2.ognl.SecurityMemberAccess; +import org.apache.struts2.ognl.StrutsProxyCacheFactory; +import org.apache.struts2.util.ProxyService; +import org.jboss.weld.bootstrap.api.helpers.RegistrySingletonProvider; +import org.jboss.weld.environment.se.Weld; +import org.jboss.weld.environment.se.WeldContainer; +import org.jboss.weld.proxy.WeldClientProxy; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.lang.reflect.Member; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CdiSecurityMemberAccessProxyTest { + + private static WeldContainer container; + + private OgnlContext context; + private SecurityMemberAccess sma; + private ProxiedFooService proxy; + private Member proxyMember; // WeldClientProxy#getMetadata — a proxy member + private Member realMember; // ProxiedFooService#getHello — a real bean member + + @BeforeClass + public static void startContainer() { + container = new Weld().containerId(RegistrySingletonProvider.STATIC_INSTANCE).initialize(); + } + + @AfterClass + public static void stopContainer() { + container.shutdown(); + } + + @Before + public void setUp() throws Exception { + ProxyService proxyService = new CdiProxyService(new StrutsProxyCacheFactory<>("1000", "basic")); + sma = new SecurityMemberAccess(null, null); + sma.setProxyService(proxyService); + + context = (OgnlContext) Ognl.createDefaultContext(null); + proxy = container.select(ProxiedFooService.class).get(); + proxyMember = proxy.getClass().getMethod("getMetadata"); + realMember = proxy.getClass().getMethod("getHello"); + + assertThat(proxy).isInstanceOf(WeldClientProxy.class); // sanity + } + + @Test + public void disallowProxyObjectAccessBlocksWeldProxy() { + sma.useDisallowProxyObjectAccess(Boolean.TRUE.toString()); + // Object-level proxy access disallowed -> any member on the proxy is blocked. + assertThat(sma.isAccessible(context, proxy, realMember, "")).isFalse(); + assertThat(sma.isAccessible(context, proxy, proxyMember, "")).isFalse(); + } + + @Test + public void disallowProxyMemberAccessBlocksWeldProxyMember() { + sma.useDisallowProxyObjectAccess(Boolean.FALSE.toString()); + sma.useDisallowProxyMemberAccess(Boolean.TRUE.toString()); + // The Weld proxy accessor is a proxy member -> blocked. + assertThat(sma.isAccessible(context, proxy, proxyMember, "")).isFalse(); + } +} From 196b105d0f48d80b3c6d40cfcf8d147a6e2fd7e9 Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Mon, 20 Jul 2026 14:28:30 +0200 Subject: [PATCH 3/6] WW-5604 Address review: positive allowlist test, guard Weld member check, fix javadoc Co-Authored-By: Claude Opus 4.8 --- .../apache/struts2/cdi/CdiProxyService.java | 11 +++++++--- .../CdiSecurityMemberAccessProxyTest.java | 22 ++++++++++++++++--- 2 files changed, 27 insertions(+), 6 deletions(-) rename plugins/cdi/src/test/java/org/apache/struts2/{cdi => ognl}/CdiSecurityMemberAccessProxyTest.java (79%) diff --git a/plugins/cdi/src/main/java/org/apache/struts2/cdi/CdiProxyService.java b/plugins/cdi/src/main/java/org/apache/struts2/cdi/CdiProxyService.java index 637287bb86..67dba2cc82 100644 --- a/plugins/cdi/src/main/java/org/apache/struts2/cdi/CdiProxyService.java +++ b/plugins/cdi/src/main/java/org/apache/struts2/cdi/CdiProxyService.java @@ -27,13 +27,15 @@ import java.lang.reflect.Member; import java.lang.reflect.Method; +import static java.lang.reflect.Modifier.isStatic; + /** * CDI-aware {@link org.apache.struts2.util.ProxyService}. Extends the default * {@link StrutsProxyService} (Spring + Hibernate detection) with recognition of * Weld client proxies, so {@code SecurityMemberAccess} can resolve the real * target class of a normal-scoped CDI bean before evaluating the OGNL allowlist. * - * @see WW-5604 + * @see WW-5604 */ public class CdiProxyService extends StrutsProxyService { @@ -49,7 +51,7 @@ public boolean isProxy(Object object) { @Override public boolean isProxyMember(Member member, Object object) { - return super.isProxyMember(member, object) || isWeldProxyMember(member); + return super.isProxyMember(member, object) || isWeldProxyMember(member, object); } @Override @@ -80,7 +82,10 @@ private Class weldUltimateTargetClass(Object candidate) { } } - private boolean isWeldProxyMember(Member member) { + private boolean isWeldProxyMember(Member member, Object object) { + if (!isStatic(member.getModifiers()) && !isWeldProxy(object)) { + return false; + } try { if (member instanceof Method method) { return MethodUtils.getMatchingMethod(WeldClientProxy.class, member.getName(), method.getParameterTypes()) != null; diff --git a/plugins/cdi/src/test/java/org/apache/struts2/cdi/CdiSecurityMemberAccessProxyTest.java b/plugins/cdi/src/test/java/org/apache/struts2/ognl/CdiSecurityMemberAccessProxyTest.java similarity index 79% rename from plugins/cdi/src/test/java/org/apache/struts2/cdi/CdiSecurityMemberAccessProxyTest.java rename to plugins/cdi/src/test/java/org/apache/struts2/ognl/CdiSecurityMemberAccessProxyTest.java index 6d6a74914e..fe1e6daf61 100644 --- a/plugins/cdi/src/test/java/org/apache/struts2/cdi/CdiSecurityMemberAccessProxyTest.java +++ b/plugins/cdi/src/test/java/org/apache/struts2/ognl/CdiSecurityMemberAccessProxyTest.java @@ -16,12 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.struts2.cdi; +package org.apache.struts2.ognl; import ognl.Ognl; import ognl.OgnlContext; -import org.apache.struts2.ognl.SecurityMemberAccess; -import org.apache.struts2.ognl.StrutsProxyCacheFactory; +import org.apache.struts2.cdi.CdiProxyService; +import org.apache.struts2.cdi.ProxiedFooService; import org.apache.struts2.util.ProxyService; import org.jboss.weld.bootstrap.api.helpers.RegistrySingletonProvider; import org.jboss.weld.environment.se.Weld; @@ -33,6 +33,7 @@ import org.junit.Test; import java.lang.reflect.Member; +import java.lang.reflect.Method; import static org.assertj.core.api.Assertions.assertThat; @@ -85,4 +86,19 @@ public void disallowProxyMemberAccessBlocksWeldProxyMember() { // The Weld proxy accessor is a proxy member -> blocked. assertThat(sma.isAccessible(context, proxy, proxyMember, "")).isFalse(); } + + /** + * When the allowlist is enabled and proxy object access is allowed, a Weld client proxy must be allowlisted based + * on its underlying target class ({@code ProxiedFooService}), not the proxy class. This is the core WW-5604 scenario. + */ + @Test + public void classInclusion_weldProxy_allowProxyObjectAccess() throws Exception { + Method realMethod = proxy.getClass().getMethod("getHello"); + + sma.useEnforceAllowlistEnabled(Boolean.TRUE.toString()); + sma.useDisallowProxyObjectAccess(Boolean.FALSE.toString()); + sma.useAllowlistClasses(ProxiedFooService.class.getName()); + + assertThat(sma.checkAllowlist(proxy, realMethod)).isTrue(); + } } From c4def1ab3b48b065b6c08cf17f6864d84a08606e Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Mon, 20 Jul 2026 14:41:50 +0200 Subject: [PATCH 4/6] WW-5604 Add WELD_AVAILABLE guard and weld-api version property Co-Authored-By: Claude Opus 4.8 --- plugins/cdi/pom.xml | 4 +++- .../apache/struts2/cdi/CdiProxyService.java | 19 ++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/plugins/cdi/pom.xml b/plugins/cdi/pom.xml index 696f976492..90910553be 100644 --- a/plugins/cdi/pom.xml +++ b/plugins/cdi/pom.xml @@ -42,7 +42,7 @@ org.jboss.weld weld-api - 6.0.Final + ${weld-api.version} provided @@ -78,5 +78,7 @@ UTF-8 + + 6.0.Final diff --git a/plugins/cdi/src/main/java/org/apache/struts2/cdi/CdiProxyService.java b/plugins/cdi/src/main/java/org/apache/struts2/cdi/CdiProxyService.java index 67dba2cc82..3a6e5e6038 100644 --- a/plugins/cdi/src/main/java/org/apache/struts2/cdi/CdiProxyService.java +++ b/plugins/cdi/src/main/java/org/apache/struts2/cdi/CdiProxyService.java @@ -39,6 +39,17 @@ */ public class CdiProxyService extends StrutsProxyService { + private static final boolean WELD_AVAILABLE = isWeldAvailable(); + + private static boolean isWeldAvailable() { + try { + Class.forName("org.jboss.weld.proxy.WeldClientProxy"); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + @Inject public CdiProxyService(ProxyCacheFactory proxyCacheFactory) { super(proxyCacheFactory); @@ -63,7 +74,7 @@ public Class ultimateTargetClass(Object candidate) { } private boolean isWeldProxy(Object object) { - if (object == null) { + if (!WELD_AVAILABLE || object == null) { return false; } try { @@ -74,6 +85,9 @@ private boolean isWeldProxy(Object object) { } private Class weldUltimateTargetClass(Object candidate) { + if (!WELD_AVAILABLE) { + return candidate.getClass(); + } try { Object instance = ((WeldClientProxy) candidate).getMetadata().getContextualInstance(); return instance != null ? instance.getClass() : candidate.getClass(); @@ -83,6 +97,9 @@ private Class weldUltimateTargetClass(Object candidate) { } private boolean isWeldProxyMember(Member member, Object object) { + if (!WELD_AVAILABLE) { + return false; + } if (!isStatic(member.getModifiers()) && !isWeldProxy(object)) { return false; } From affaf5e83d06942dce7600feb104f70e20a3fc39 Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Mon, 20 Jul 2026 18:17:03 +0200 Subject: [PATCH 5/6] WW-5604 Cover null, non-proxy, non-method and Weld-absent paths Co-Authored-By: Claude Opus 4.8 --- .../apache/struts2/cdi/CdiProxyService.java | 16 +++++++-- .../struts2/cdi/CdiProxyServiceTest.java | 33 +++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/plugins/cdi/src/main/java/org/apache/struts2/cdi/CdiProxyService.java b/plugins/cdi/src/main/java/org/apache/struts2/cdi/CdiProxyService.java index 3a6e5e6038..b451548a8e 100644 --- a/plugins/cdi/src/main/java/org/apache/struts2/cdi/CdiProxyService.java +++ b/plugins/cdi/src/main/java/org/apache/struts2/cdi/CdiProxyService.java @@ -50,9 +50,19 @@ private static boolean isWeldAvailable() { } } + private final boolean weldAvailable; + @Inject public CdiProxyService(ProxyCacheFactory proxyCacheFactory) { + this(proxyCacheFactory, WELD_AVAILABLE); + } + + /** + * Package-private constructor so tests can exercise the Weld-absent fallback. + */ + CdiProxyService(ProxyCacheFactory proxyCacheFactory, boolean weldAvailable) { super(proxyCacheFactory); + this.weldAvailable = weldAvailable; } @Override @@ -74,7 +84,7 @@ public Class ultimateTargetClass(Object candidate) { } private boolean isWeldProxy(Object object) { - if (!WELD_AVAILABLE || object == null) { + if (!weldAvailable || object == null) { return false; } try { @@ -85,7 +95,7 @@ private boolean isWeldProxy(Object object) { } private Class weldUltimateTargetClass(Object candidate) { - if (!WELD_AVAILABLE) { + if (!weldAvailable) { return candidate.getClass(); } try { @@ -97,7 +107,7 @@ private Class weldUltimateTargetClass(Object candidate) { } private boolean isWeldProxyMember(Member member, Object object) { - if (!WELD_AVAILABLE) { + if (!weldAvailable) { return false; } if (!isStatic(member.getModifiers()) && !isWeldProxy(object)) { diff --git a/plugins/cdi/src/test/java/org/apache/struts2/cdi/CdiProxyServiceTest.java b/plugins/cdi/src/test/java/org/apache/struts2/cdi/CdiProxyServiceTest.java index 92013c823e..92f6911aa7 100644 --- a/plugins/cdi/src/test/java/org/apache/struts2/cdi/CdiProxyServiceTest.java +++ b/plugins/cdi/src/test/java/org/apache/struts2/cdi/CdiProxyServiceTest.java @@ -29,6 +29,7 @@ import org.junit.BeforeClass; import org.junit.Test; +import java.lang.reflect.Field; import java.lang.reflect.Method; import static org.assertj.core.api.Assertions.assertThat; @@ -85,4 +86,36 @@ public void plainObjectIsNotProxy() { assertThat(proxyService.isProxy(plain)).isFalse(); assertThat(proxyService.ultimateTargetClass(plain)).isEqualTo(Object.class); } + + @Test + public void nullIsNotProxy() { + assertThat(proxyService.isProxy(null)).isFalse(); + } + + @Test + public void memberOfNonProxyObjectIsNotProxyMember() throws Exception { + Method getHello = ProxiedFooService.class.getMethod("getHello"); + assertThat(proxyService.isProxyMember(getHello, new Object())).isFalse(); + } + + @Test + public void nonMethodMemberIsNotProxyMember() throws Exception { + Field field = SomeHolder.class.getDeclaredField("value"); + assertThat(proxyService.isProxyMember(field, proxy)).isFalse(); + } + + @Test + public void withoutWeldFallsBackToBaseBehaviour() throws Exception { + ProxyService noWeld = new CdiProxyService(new StrutsProxyCacheFactory<>("1000", "basic"), false); + Method getMetadata = proxy.getClass().getMethod("getMetadata"); + + assertThat(noWeld.isProxy(proxy)).isFalse(); + assertThat(noWeld.ultimateTargetClass(proxy)).isEqualTo(proxy.getClass()); + assertThat(noWeld.isProxyMember(getMetadata, proxy)).isFalse(); + } + + private static class SomeHolder { + @SuppressWarnings("unused") + private String value; + } } From 7fefd98dfb63dcb7c3f7ee69e987fb3fb49a92d7 Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Mon, 20 Jul 2026 19:42:56 +0200 Subject: [PATCH 6/6] WW-5604 Remove unreachable guard and cover unwrap fallbacks Co-Authored-By: Claude Opus 4.8 --- .../apache/struts2/cdi/CdiProxyService.java | 3 -- .../struts2/cdi/CdiProxyServiceTest.java | 39 +++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/plugins/cdi/src/main/java/org/apache/struts2/cdi/CdiProxyService.java b/plugins/cdi/src/main/java/org/apache/struts2/cdi/CdiProxyService.java index b451548a8e..b5e4e1b5d0 100644 --- a/plugins/cdi/src/main/java/org/apache/struts2/cdi/CdiProxyService.java +++ b/plugins/cdi/src/main/java/org/apache/struts2/cdi/CdiProxyService.java @@ -95,9 +95,6 @@ private boolean isWeldProxy(Object object) { } private Class weldUltimateTargetClass(Object candidate) { - if (!weldAvailable) { - return candidate.getClass(); - } try { Object instance = ((WeldClientProxy) candidate).getMetadata().getContextualInstance(); return instance != null ? instance.getClass() : candidate.getClass(); diff --git a/plugins/cdi/src/test/java/org/apache/struts2/cdi/CdiProxyServiceTest.java b/plugins/cdi/src/test/java/org/apache/struts2/cdi/CdiProxyServiceTest.java index 92f6911aa7..d75ca4cbc7 100644 --- a/plugins/cdi/src/test/java/org/apache/struts2/cdi/CdiProxyServiceTest.java +++ b/plugins/cdi/src/test/java/org/apache/struts2/cdi/CdiProxyServiceTest.java @@ -18,6 +18,7 @@ */ package org.apache.struts2.cdi; +import jakarta.enterprise.inject.spi.Bean; import org.apache.struts2.ognl.StrutsProxyCacheFactory; import org.apache.struts2.util.ProxyService; import org.jboss.weld.bootstrap.api.helpers.RegistrySingletonProvider; @@ -114,8 +115,46 @@ public void withoutWeldFallsBackToBaseBehaviour() throws Exception { assertThat(noWeld.isProxyMember(getMetadata, proxy)).isFalse(); } + @Test + public void ultimateTargetClassFallsBackToProxyClassWhenContextualInstanceIsNull() { + // Contract: a null contextual instance from Weld's Metadata must fall back to the proxy's own class, not NPE or return null + NullContextualInstanceProxy stub = new NullContextualInstanceProxy(); + assertThat(proxyService.ultimateTargetClass(stub)).isEqualTo(stub.getClass()); + } + + @Test + public void ultimateTargetClassFallsBackToProxyClassWhenUnwrappingThrowsLinkageError() { + // Contract: a LinkageError while unwrapping the proxy must fall back to the proxy's own class rather than propagate + LinkageErrorProxy stub = new LinkageErrorProxy(); + assertThat(proxyService.ultimateTargetClass(stub)).isEqualTo(stub.getClass()); + } + private static class SomeHolder { @SuppressWarnings("unused") private String value; } + + private static class NullContextualInstanceProxy implements WeldClientProxy { + @Override + public Metadata getMetadata() { + return new Metadata() { + @Override + public Bean getBean() { + return null; + } + + @Override + public Object getContextualInstance() { + return null; + } + }; + } + } + + private static class LinkageErrorProxy implements WeldClientProxy { + @Override + public Metadata getMetadata() { + throw new NoClassDefFoundError("simulated missing Weld class"); + } + } }