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 <noreply@anthropic.com>

* WW-5604 Register CdiProxyService as the active ProxyService

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* WW-5604 Address review: positive allowlist test, guard Weld member check, fix javadoc

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* WW-5604 Add WELD_AVAILABLE guard and weld-api version property

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* WW-5604 Cover null, non-proxy, non-method and Weld-absent paths

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* WW-5604 Remove unreachable guard and cover unwrap fallbacks

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Lukasz Lenart
2026-07-21 10:58:28 +02:00
committed by GitHub
parent 464817e0b0
commit f2c1f50da2
6 changed files with 431 additions and 0 deletions
+9
View File
@@ -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>
@@ -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;
}
}
}
@@ -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>
@@ -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");
}
}
}
@@ -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";
}
}
@@ -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();
}
}