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();
+    }
+}

Reply via email to