mirror of
https://github.com/apache/struts.git
synced 2026-08-05 06:36:58 +00:00
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:
@@ -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";
|
||||
}
|
||||
}
|
||||
+104
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user