This is an automated email from the ASF dual-hosted git repository.
lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git
The following commit(s) were added to refs/heads/main by this push:
new f2c1f50da WW-5604 Recognize CDI/Weld client proxies in
SecurityMemberAccess (#1796)
f2c1f50da is described below
commit f2c1f50da201eeeafc498287499906e3e75792b3
Author: Lukasz Lenart <[email protected]>
AuthorDate: Tue Jul 21 10:58:28 2026 +0200
WW-5604 Recognize CDI/Weld client proxies in SecurityMemberAccess (#1796)
* WW-5604 Add CdiProxyService to detect Weld client proxies
Co-Authored-By: Claude Opus 4.8 <[email protected]>
* WW-5604 Register CdiProxyService as the active ProxyService
Co-Authored-By: Claude Opus 4.8 <[email protected]>
* WW-5604 Address review: positive allowlist test, guard Weld member check,
fix javadoc
Co-Authored-By: Claude Opus 4.8 <[email protected]>
* WW-5604 Add WELD_AVAILABLE guard and weld-api version property
Co-Authored-By: Claude Opus 4.8 <[email protected]>
* WW-5604 Cover null, non-proxy, non-method and Weld-absent paths
Co-Authored-By: Claude Opus 4.8 <[email protected]>
* WW-5604 Remove unreachable guard and cover unwrap fallbacks
Co-Authored-By: Claude Opus 4.8 <[email protected]>
---------
Co-authored-by: Claude Opus 4.8 <[email protected]>
---
plugins/cdi/pom.xml | 9 ++
.../org/apache/struts2/cdi/CdiProxyService.java | 122 ++++++++++++++++
plugins/cdi/src/main/resources/struts-plugin.xml | 3 +
.../apache/struts2/cdi/CdiProxyServiceTest.java | 160 +++++++++++++++++++++
.../org/apache/struts2/cdi/ProxiedFooService.java} | 24 ++--
.../ognl/CdiSecurityMemberAccessProxyTest.java | 104 ++++++++++++++
6 files changed, 410 insertions(+), 12 deletions(-)
diff --git a/plugins/cdi/pom.xml b/plugins/cdi/pom.xml
index 12a39b59c..90910553b 100644
--- a/plugins/cdi/pom.xml
+++ b/plugins/cdi/pom.xml
@@ -39,6 +39,13 @@
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>org.jboss.weld</groupId>
+ <artifactId>weld-api</artifactId>
+ <version>${weld-api.version}</version>
+ <scope>provided</scope>
+ </dependency>
+
<dependency>
<groupId>org.jboss.weld</groupId>
<artifactId>weld-core-impl</artifactId>
@@ -71,5 +78,7 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <!-- weld-api has its own version line, distinct from ${weld.version}
(weld-core/weld-se) -->
+ <weld-api.version>6.0.Final</weld-api.version>
</properties>
</project>
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 000000000..b5e4e1b5d
--- /dev/null
+++ b/plugins/cdi/src/main/java/org/apache/struts2/cdi/CdiProxyService.java
@@ -0,0 +1,122 @@
+/*
+ * 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;
+
+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 <a href="https://issues.apache.org/jira/browse/WW-5604">WW-5604</a>
+ */
+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;
+ }
+ }
+
+ 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
+ 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, object);
+ }
+
+ @Override
+ public Class<?> ultimateTargetClass(Object candidate) {
+ if (isWeldProxy(candidate)) {
+ return weldUltimateTargetClass(candidate);
+ }
+ return super.ultimateTargetClass(candidate);
+ }
+
+ private boolean isWeldProxy(Object object) {
+ if (!weldAvailable || 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, Object object) {
+ if (!weldAvailable) {
+ return false;
+ }
+ if (!isStatic(member.getModifiers()) && !isWeldProxy(object)) {
+ return false;
+ }
+ 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/main/resources/struts-plugin.xml
b/plugins/cdi/src/main/resources/struts-plugin.xml
index b0458e940..c69b2073f 100644
--- a/plugins/cdi/src/main/resources/struts-plugin.xml
+++ b/plugins/cdi/src/main/resources/struts-plugin.xml
@@ -30,4 +30,7 @@
<!-- Make the CDI object factory the automatic default -->
<constant name="struts.objectFactory" value="cdi" />
+ <bean type="org.apache.struts2.util.ProxyService" name="cdi"
class="org.apache.struts2.cdi.CdiProxyService" />
+ <constant name="struts.proxyService" value="cdi" />
+
</struts>
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 000000000..d75ca4cbc
--- /dev/null
+++ b/plugins/cdi/src/test/java/org/apache/struts2/cdi/CdiProxyServiceTest.java
@@ -0,0 +1,160 @@
+/*
+ * 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.inject.spi.Bean;
+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.Field;
+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);
+ }
+
+ @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();
+ }
+
+ @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");
+ }
+ }
+}
diff --git a/plugins/cdi/src/main/resources/struts-plugin.xml
b/plugins/cdi/src/test/java/org/apache/struts2/cdi/ProxiedFooService.java
similarity index 64%
copy from plugins/cdi/src/main/resources/struts-plugin.xml
copy to plugins/cdi/src/test/java/org/apache/struts2/cdi/ProxiedFooService.java
index b0458e940..45b824d82 100644
--- a/plugins/cdi/src/main/resources/struts-plugin.xml
+++ b/plugins/cdi/src/test/java/org/apache/struts2/cdi/ProxiedFooService.java
@@ -1,5 +1,3 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
@@ -18,16 +16,18 @@
* specific language governing permissions and limitations
* under the License.
*/
--->
-<!DOCTYPE struts PUBLIC
- "-//Apache Software Foundation//DTD Struts Configuration 6.0//EN"
- "https://struts.apache.org/dtds/struts-6.0.dtd">
+package org.apache.struts2.cdi;
-<struts>
+import jakarta.enterprise.context.ApplicationScoped;
- <bean type="org.apache.struts2.ObjectFactory" name="cdi"
class="org.apache.struts2.cdi.CdiObjectFactory" />
-
- <!-- Make the CDI object factory the automatic default -->
- <constant name="struts.objectFactory" value="cdi" />
+/**
+ * A normal-scoped CDI bean. Weld serves it through a client proxy that
+ * implements {@link org.jboss.weld.proxy.WeldClientProxy}.
+ */
+@ApplicationScoped
+public class ProxiedFooService {
-</struts>
+ public String getHello() {
+ return "Hello";
+ }
+}
diff --git
a/plugins/cdi/src/test/java/org/apache/struts2/ognl/CdiSecurityMemberAccessProxyTest.java
b/plugins/cdi/src/test/java/org/apache/struts2/ognl/CdiSecurityMemberAccessProxyTest.java
new file mode 100644
index 000000000..fe1e6daf6
--- /dev/null
+++
b/plugins/cdi/src/test/java/org/apache/struts2/ognl/CdiSecurityMemberAccessProxyTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.ognl;
+
+import ognl.Ognl;
+import ognl.OgnlContext;
+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;
+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 java.lang.reflect.Method;
+
+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();
+ }
+
+ /**
+ * 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();
+ }
+}