WW-5514 Add StrutsProxyService for proxy detection and resolution (#1586)

* feat(proxy): WW-5514 add StrutsProxyService for proxy detection and resolution

Introduces a configurable ProxyService interface and StrutsProxyService
implementation for detecting and resolving Spring AOP/Hibernate proxies.

Key changes:
- Add ProxyService interface with isProxy, ultimateTargetClass, and
  resolveTargetMember methods
- Add StrutsProxyService implementation using configurable caches
- Add ProxyCacheFactory and StrutsProxyCacheFactory for cache management
- Integrate ProxyService into ChainingInterceptor, ParametersInterceptor,
  and SecurityMemberAccess
- Add integration test with Spring AOP proxied action chaining
- Add configuration constants for proxy cache type and size

The StrutsProxyService correctly handles:
- Spring CGLIB proxies (class-based)
- Spring JDK dynamic proxies (interface-based)
- Hibernate entity proxies
- Member resolution for allowlist checking

Fixes WW-5514

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* test(proxy): WW-5514 add ProxyService integration tests for Spring proxies

Add integration tests to SpringProxyUtilTest that verify the new
ProxyService works correctly with real Spring AOP proxies, alongside
the existing deprecated ProxyUtil tests.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(proxy): WW-5514 address PR review feedback for proxy caches

Remove targetClassCache from StrutsProxyService to avoid memory leak
(object-keyed cache reintroduced from PR #1578). Change default proxy
cache type to wtlfu to align with all other caches. Switch deprecated
ProxyUtil static caches to BASIC to remove hard Caffeine dependency.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Lukasz Lenart
2026-02-21 18:18:08 +01:00
committed by GitHub
parent a9ce3e3c99
commit ca740ed8fb
30 changed files with 2100 additions and 80 deletions
@@ -522,6 +522,35 @@ public final class StrutsConstants {
*/
public static final String STRUTS_OGNL_EXPRESSION_CACHE_MAXSIZE = "struts.ognl.expressionCacheMaxSize";
/**
* Specifies the type of cache to use for proxy detection. Valid values defined in
* {@link org.apache.struts2.ognl.OgnlCacheFactory.CacheType}.
*
* @since 7.2.0
*/
public static final String STRUTS_PROXY_CACHE_TYPE = "struts.proxy.cacheType";
/**
* Specifies the maximum cache size for proxy detection caches.
*
* @since 7.2.0
*/
public static final String STRUTS_PROXY_CACHE_MAXSIZE = "struts.proxy.cacheMaxSize";
/**
* The {@link org.apache.struts2.ognl.ProxyCacheFactory} implementation class.
*
* @since 7.2.0
*/
public static final String STRUTS_PROXY_CACHE_FACTORY = "struts.proxy.cacheFactory";
/**
* The {@link org.apache.struts2.util.ProxyService} implementation class.
*
* @since 7.2.0
*/
public static final String STRUTS_PROXYSERVICE = "struts.proxyService";
/**
* Enables evaluation of OGNL expressions
*
@@ -61,6 +61,7 @@ import org.apache.struts2.interceptor.exec.ExecutorProvider;
import org.apache.struts2.ognl.BeanInfoCacheFactory;
import org.apache.struts2.ognl.ExpressionCacheFactory;
import org.apache.struts2.ognl.OgnlGuard;
import org.apache.struts2.ognl.ProxyCacheFactory;
import org.apache.struts2.ognl.SecurityMemberAccess;
import org.apache.struts2.ognl.accessor.RootAccessor;
import org.apache.struts2.security.AcceptedPatternsChecker;
@@ -72,6 +73,7 @@ import org.apache.struts2.url.UrlDecoder;
import org.apache.struts2.url.UrlEncoder;
import org.apache.struts2.util.ContentTypeMatcher;
import org.apache.struts2.util.PatternMatcher;
import org.apache.struts2.util.ProxyService;
import org.apache.struts2.util.TextParser;
import org.apache.struts2.util.ValueStackFactory;
import org.apache.struts2.util.location.LocatableProperties;
@@ -442,6 +444,8 @@ public class StrutsBeanSelectionProvider extends AbstractBeanSelectionProvider {
alias(ExpressionCacheFactory.class, StrutsConstants.STRUTS_OGNL_EXPRESSION_CACHE_FACTORY, builder, props, Scope.SINGLETON);
alias(BeanInfoCacheFactory.class, StrutsConstants.STRUTS_OGNL_BEANINFO_CACHE_FACTORY, builder, props, Scope.SINGLETON);
alias(ProxyCacheFactory.class, StrutsConstants.STRUTS_PROXY_CACHE_FACTORY, builder, props, Scope.SINGLETON);
alias(ProxyService.class, StrutsConstants.STRUTS_PROXYSERVICE, builder, props, Scope.SINGLETON);
alias(SecurityMemberAccess.class, StrutsConstants.STRUTS_MEMBER_ACCESS, builder, props, Scope.PROTOTYPE);
alias(OgnlGuard.class, StrutsConstants.STRUTS_OGNL_GUARD, builder, props, Scope.SINGLETON);
@@ -85,13 +85,17 @@ import org.apache.struts2.ognl.ExpressionCacheFactory;
import org.apache.struts2.ognl.OgnlCacheFactory;
import org.apache.struts2.ognl.OgnlReflectionProvider;
import org.apache.struts2.ognl.OgnlUtil;
import org.apache.struts2.ognl.ProxyCacheFactory;
import org.apache.struts2.ognl.StrutsProxyCacheFactory;
import org.apache.struts2.ognl.OgnlValueStackFactory;
import org.apache.struts2.ognl.SecurityMemberAccess;
import org.apache.struts2.ognl.accessor.CompoundRootAccessor;
import org.apache.struts2.ognl.accessor.RootAccessor;
import org.apache.struts2.ognl.accessor.XWorkMethodAccessor;
import org.apache.struts2.util.StrutsProxyService;
import org.apache.struts2.util.OgnlTextParser;
import org.apache.struts2.util.PatternMatcher;
import org.apache.struts2.util.ProxyService;
import org.apache.struts2.text.StrutsLocalizedTextProvider;
import org.apache.struts2.util.TextParser;
import org.apache.struts2.util.ValueStack;
@@ -144,6 +148,8 @@ public class DefaultConfiguration implements Configuration {
constants.put(StrutsConstants.STRUTS_OGNL_EXPRESSION_CACHE_MAXSIZE, 10000);
constants.put(StrutsConstants.STRUTS_OGNL_BEANINFO_CACHE_TYPE, OgnlCacheFactory.CacheType.BASIC);
constants.put(StrutsConstants.STRUTS_OGNL_BEANINFO_CACHE_MAXSIZE, 10000);
constants.put(StrutsConstants.STRUTS_PROXY_CACHE_TYPE, OgnlCacheFactory.CacheType.BASIC);
constants.put(StrutsConstants.STRUTS_PROXY_CACHE_MAXSIZE, 10000);
constants.put(StrutsConstants.STRUTS_ENABLE_DYNAMIC_METHOD_INVOCATION, Boolean.FALSE);
BOOTSTRAP_CONSTANTS = Collections.unmodifiableMap(constants);
}
@@ -395,6 +401,8 @@ public class DefaultConfiguration implements Configuration {
.factory(ExpressionCacheFactory.class, DefaultOgnlExpressionCacheFactory.class, Scope.SINGLETON)
.factory(BeanInfoCacheFactory.class, DefaultOgnlBeanInfoCacheFactory.class, Scope.SINGLETON)
.factory(ProxyCacheFactory.class, StrutsProxyCacheFactory.class, Scope.SINGLETON)
.factory(ProxyService.class, StrutsProxyService.class, Scope.SINGLETON)
.factory(OgnlUtil.class, Scope.SINGLETON)
.factory(SecurityMemberAccess.class, Scope.PROTOTYPE)
.factory(OgnlGuard.class, StrutsOgnlGuard.class, Scope.SINGLETON)
@@ -27,7 +27,7 @@ import org.apache.struts2.inject.Inject;
import org.apache.struts2.result.ActionChainResult;
import org.apache.struts2.result.Result;
import org.apache.struts2.util.CompoundRoot;
import org.apache.struts2.util.ProxyUtil;
import org.apache.struts2.util.ProxyService;
import org.apache.struts2.util.TextParseUtil;
import org.apache.struts2.util.ValueStack;
import org.apache.struts2.util.reflection.ReflectionProvider;
@@ -96,7 +96,7 @@ import java.util.Map;
* </p>
* <!-- END SNIPPET: extending -->
* <u>Example code:</u>
*
* <p>
* <!-- START SNIPPET: example -->
* <pre>
* &lt;action name="someAction" class="com.examples.SomeAction"&gt;
@@ -114,7 +114,6 @@ import java.util.Map;
* </pre>
* <!-- END SNIPPET: example -->
*
*
* @author mrdon
* @author tm_jee ( tm_jee(at)yahoo.co.uk )
* @see ActionChainResult
@@ -135,12 +134,18 @@ public class ChainingInterceptor extends AbstractInterceptor {
protected Collection<String> includes;
protected ReflectionProvider reflectionProvider;
private ProxyService proxyService;
@Inject
public void setReflectionProvider(ReflectionProvider prov) {
this.reflectionProvider = prov;
}
@Inject
public void setProxyService(ProxyService proxyService) {
this.proxyService = proxyService;
}
@Inject(value = StrutsConstants.STRUTS_CHAINING_COPY_ERRORS, required = false)
public void setCopyErrors(String copyErrors) {
this.copyErrors = "true".equalsIgnoreCase(copyErrors);
@@ -175,8 +180,8 @@ public class ChainingInterceptor extends AbstractInterceptor {
}
Object action = invocation.getAction();
Class<?> editable = null;
if (ProxyUtil.isProxy(action)) {
editable = ProxyUtil.ultimateTargetClass(action);
if (proxyService.isProxy(action)) {
editable = proxyService.ultimateTargetClass(action);
}
reflectionProvider.copy(object, action, ctxMap, prepareExcludes(), includes, editable);
}
@@ -184,7 +189,7 @@ public class ChainingInterceptor extends AbstractInterceptor {
private Collection<String> prepareExcludes() {
Collection<String> localExcludes = excludes;
if (!copyErrors || !copyMessages ||!copyFieldErrors) {
if (!copyErrors || !copyMessages || !copyFieldErrors) {
if (localExcludes == null) {
localExcludes = new HashSet<>();
if (!copyErrors) {
@@ -39,7 +39,7 @@ import org.apache.struts2.security.DefaultAcceptedPatternsChecker;
import org.apache.struts2.security.ExcludedPatternsChecker;
import org.apache.struts2.util.ClearableValueStack;
import org.apache.struts2.util.MemberAccessValueStack;
import org.apache.struts2.util.ProxyUtil;
import org.apache.struts2.util.ProxyService;
import org.apache.struts2.util.TextParseUtil;
import org.apache.struts2.util.ValueStack;
import org.apache.struts2.util.ValueStackFactory;
@@ -95,6 +95,7 @@ public class ParametersInterceptor extends MethodFilterInterceptor {
private ValueStackFactory valueStackFactory;
private OgnlUtil ognlUtil;
protected ThreadAllowlist threadAllowlist;
private ProxyService proxyService;
private ExcludedPatternsChecker excludedPatterns;
private AcceptedPatternsChecker acceptedPatterns;
private Set<Pattern> excludedValuePatterns = null;
@@ -115,6 +116,11 @@ public class ParametersInterceptor extends MethodFilterInterceptor {
this.threadAllowlist = threadAllowlist;
}
@Inject
public void setProxyService(ProxyService proxyService) {
this.proxyService = proxyService;
}
@Inject(StrutsConstants.STRUTS_DEVMODE)
public void setDevMode(String mode) {
this.devMode = BooleanUtils.toBoolean(mode);
@@ -516,8 +522,8 @@ public class ParametersInterceptor extends MethodFilterInterceptor {
}
protected Class<?> ultimateClass(Object action) {
if (ProxyUtil.isProxy(action)) {
return ProxyUtil.ultimateTargetClass(action);
if (proxyService.isProxy(action)) {
return proxyService.ultimateTargetClass(action);
}
return action.getClass();
}
@@ -0,0 +1,27 @@
/*
* Copyright 2022 Apache Software Foundation.
*
* Licensed 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;
/**
* A proxy interface to be used with Struts DI mechanism for proxy detection caching.
*
* @param &lt;Key&gt; The type for the cache key entries
* @param &lt;Value&gt; The type for the cache value entries
* @since 7.2.0
*/
public interface ProxyCacheFactory<Key, Value> extends OgnlCacheFactory<Key, Value> {
}
@@ -25,7 +25,7 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.util.ProxyUtil;
import org.apache.struts2.util.ProxyService;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
@@ -76,6 +76,8 @@ public class SecurityMemberAccess implements MemberAccess {
private final ProviderAllowlist providerAllowlist;
private final ThreadAllowlist threadAllowlist;
private ProxyService proxyService;
private boolean allowStaticFieldAccess = true;
private Set<Pattern> excludeProperties = emptySet();
@@ -107,6 +109,11 @@ public class SecurityMemberAccess implements MemberAccess {
this.threadAllowlist = threadAllowlist;
}
@Inject
public void setProxyService(ProxyService proxyService) {
this.proxyService = proxyService;
}
@Override
public Object setup(OgnlContext context, Object target, Member member, String propertyName) {
Object result = null;
@@ -214,15 +221,15 @@ public class SecurityMemberAccess implements MemberAccess {
Class<?> targetClass = target != null ? target.getClass() : null;
if (!disallowProxyObjectAccess && ProxyUtil.isProxy(target)) {
if (!disallowProxyObjectAccess && proxyService.isProxy(target)) {
// If `disallowProxyObjectAccess` is not set, allow resolving Hibernate entities and Spring proxies to their
// underlying classes/members. This allows the allowlist capability to continue working and still offer
// protection in applications where the developer has accepted the risk of allowing OGNL access to Hibernate
// entities and Spring proxies. This is preferred to having to disable the allowlist capability entirely.
Class<?> newTargetClass = ProxyUtil.ultimateTargetClass(target);
Class<?> newTargetClass = proxyService.ultimateTargetClass(target);
if (newTargetClass != targetClass) {
targetClass = newTargetClass;
member = ProxyUtil.resolveTargetMember(member, newTargetClass);
member = proxyService.resolveTargetMember(member, newTargetClass);
}
}
@@ -312,14 +319,14 @@ public class SecurityMemberAccess implements MemberAccess {
* @return {@code true} if proxy object access is allowed
*/
protected boolean checkProxyObjectAccess(Object target) {
return !(disallowProxyObjectAccess && ProxyUtil.isProxy(target));
return !(disallowProxyObjectAccess && proxyService.isProxy(target));
}
/**
* @return {@code true} if proxy member access is allowed
*/
protected boolean checkProxyMemberAccess(Object target, Member member) {
return !(disallowProxyMemberAccess && ProxyUtil.isProxyMember(member, target));
return !(disallowProxyMemberAccess && proxyService.isProxyMember(member, target));
}
/**
@@ -0,0 +1,39 @@
/*
* Copyright 2022 Apache Software Foundation.
*
* Licensed 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 org.apache.commons.lang3.EnumUtils;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.inject.Inject;
/**
* Struts proxy cache factory implementation.
* Used for creating caches for proxy detection operations.
*
* @param &lt;Key&gt; The type for the cache key entries
* @param &lt;Value&gt; The type for the cache value entries
* @since 7.2.0
*/
public class StrutsProxyCacheFactory<Key, Value> extends DefaultOgnlCacheFactory<Key, Value>
implements ProxyCacheFactory<Key, Value> {
@Inject
public StrutsProxyCacheFactory(
@Inject(value = StrutsConstants.STRUTS_PROXY_CACHE_MAXSIZE) String cacheMaxSize,
@Inject(value = StrutsConstants.STRUTS_PROXY_CACHE_TYPE) String defaultCacheType) {
super(Integer.parseInt(cacheMaxSize), EnumUtils.getEnumIgnoreCase(CacheType.class, defaultCacheType));
}
}
@@ -0,0 +1,101 @@
/*
* 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.util;
import java.lang.reflect.Member;
/**
* Service interface for proxy detection and resolution operations.
* Replaces static {@link ProxyUtil} methods with an injectable service.
*
* @since 7.2.0
*/
public interface ProxyService {
/**
* Determine the ultimate target class of the given instance, traversing
* not only a top-level proxy but any number of nested proxies as well &amp;mdash;
* as long as possible without side effects.
*
* @param candidate the instance to check (might be a proxy)
* @return the ultimate target class (or the plain class of the given
* object as fallback; never {@code null})
*/
Class<?> ultimateTargetClass(Object candidate);
/**
* Check whether the given object is a proxy.
*
* @param object the object to check
* @return true if the object is a Spring AOP or Hibernate proxy
*/
boolean isProxy(Object object);
/**
* Check whether the given member is a proxy member of a proxy object or is a static proxy member.
*
* @param member the member to check
* @param object the object to check
* @return true if the member is a proxy member
*/
boolean isProxyMember(Member member, Object object);
/**
* Check whether the given object is a Hibernate proxy.
*
* @param object the object to check
* @return true if the object is a Hibernate proxy
*/
boolean isHibernateProxy(Object object);
/**
* Check whether the given member is a member of a Hibernate proxy.
*
* @param member the member to check
* @return true if the member is a Hibernate proxy member
*/
boolean isHibernateProxyMember(Member member);
/**
* Get the target instance of the given object if it is a Hibernate proxy object,
* otherwise return the given object.
*
* @param object the object to check
* @return the target instance or the original object
*/
Object getHibernateProxyTarget(Object object);
/**
* Resolve matching member on target class.
*
* @param proxyMember the proxy member
* @param targetClass the target class
* @return matching member on target object if one exists, otherwise the same member
*/
Member resolveTargetMember(Member proxyMember, Class<?> targetClass);
/**
* @param proxyMember the proxy member
* @param target the target object
* @return matching member on target object if one exists, otherwise the same member
* @deprecated since 7.1, use {@link #resolveTargetMember(Member, Class)} instead.
*/
@Deprecated
Member resolveTargetMember(Member proxyMember, Object target);
}
@@ -43,26 +43,31 @@ import static java.lang.reflect.Modifier.isStatic;
/**
* <code>ProxyUtil</code>
* <p>
* Various utility methods dealing with proxies
* Various utility methods dealing with proxies.
* </p>
*
* @deprecated since 7.2, inject {@link ProxyService} instead. This class will be removed in a future version.
*/
@Deprecated(since = "7.2")
public class ProxyUtil {
private static final int CACHE_MAX_SIZE = 10000;
private static final int CACHE_INITIAL_CAPACITY = 256;
private static final OgnlCache<Class<?>, Boolean> isProxyCache = new DefaultOgnlCacheFactory<Class<?>, Boolean>(
CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.WTLFU, CACHE_INITIAL_CAPACITY).buildOgnlCache();
CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.BASIC, CACHE_INITIAL_CAPACITY).buildOgnlCache();
private static final OgnlCache<Member, Boolean> isProxyMemberCache = new DefaultOgnlCacheFactory<Member, Boolean>(
CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.WTLFU, CACHE_INITIAL_CAPACITY).buildOgnlCache();
CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.BASIC, CACHE_INITIAL_CAPACITY).buildOgnlCache();
/**
* Determine the ultimate target class of the given instance, traversing
* not only a top-level proxy but any number of nested proxies as well &mdash;
* as long as possible without side effects.
*
* @param candidate the instance to check (might be a proxy)
* @return the ultimate target class (or the plain class of the given
* object as fallback; never {@code null})
* @deprecated since 7.2, inject {@link ProxyService} instead
*/
@Deprecated(since = "7.2")
public static Class<?> ultimateTargetClass(Object candidate) {
Class<?> result = null;
if (isSpringAopProxy(candidate)) {
@@ -78,8 +83,12 @@ public class ProxyUtil {
/**
* Check whether the given object is a proxy.
*
* @param object the object to check
* @return true if the object is a Spring AOP or Hibernate proxy
* @deprecated since 7.2, inject {@link ProxyService} instead
*/
@Deprecated(since = "7.2")
public static boolean isProxy(Object object) {
if (object == null) return false;
return isProxyCache.computeIfAbsent(object.getClass(),
@@ -88,9 +97,13 @@ public class ProxyUtil {
/**
* Check whether the given member is a proxy member of a proxy object or is a static proxy member.
*
* @param member the member to check
* @param object the object to check
* @return true if the member is a proxy member
* @deprecated since 7.2, inject {@link ProxyService} instead
*/
@Deprecated(since = "7.2")
public static boolean isProxyMember(Member member, Object object) {
if (!isStatic(member.getModifiers()) && !isProxy(object)) {
return false;
@@ -103,7 +116,10 @@ public class ProxyUtil {
* Check whether the given object is a Hibernate proxy.
*
* @param object the object to check
* @return true if the object is a Hibernate proxy
* @deprecated since 7.2, inject {@link ProxyService} instead
*/
@Deprecated(since = "7.2")
public static boolean isHibernateProxy(Object object) {
try {
return object != null && HibernateProxy.class.isAssignableFrom(object.getClass());
@@ -116,7 +132,10 @@ public class ProxyUtil {
* Check whether the given member is a member of a Hibernate proxy.
*
* @param member the member to check
* @return true if the member is a Hibernate proxy member
* @deprecated since 7.2, inject {@link ProxyService} instead
*/
@Deprecated(since = "7.2")
public static boolean isHibernateProxyMember(Member member) {
try {
return hasMember(HibernateProxy.class, member);
@@ -129,6 +148,7 @@ public class ProxyUtil {
* Determine the ultimate target class of the given spring bean instance, traversing
* not only a top-level spring proxy but any number of nested spring proxies as well &mdash;
* as long as possible without side effects, that is, just for singleton targets.
*
* @param candidate the instance to check (might be a spring AOP proxy)
* @return the ultimate target class (or the plain class of the given
* object as fallback; never {@code null})
@@ -143,6 +163,7 @@ public class ProxyUtil {
/**
* Check whether the given object is a Spring proxy.
*
* @param object the object to check
*/
private static boolean isSpringAopProxy(Object object) {
@@ -155,6 +176,7 @@ public class ProxyUtil {
/**
* Check whether the given member is a member of a spring proxy.
*
* @param member the member to check
*/
private static boolean isSpringProxyMember(Member member) {
@@ -172,7 +194,8 @@ public class ProxyUtil {
/**
* Check whether the given class has a given member.
* @param clazz the class to check
*
* @param clazz the class to check
* @param member the member to check
*/
private static boolean hasMember(Class<?> clazz, Member member) {
@@ -189,8 +212,13 @@ public class ProxyUtil {
}
/**
* Get the target instance of the given object if it is a Hibernate proxy object.
*
* @param object the object to check
* @return the target instance of the given object if it is a Hibernate proxy object, otherwise the given object
* @deprecated since 7.2, inject {@link ProxyService} instead
*/
@Deprecated(since = "7.2")
public static Object getHibernateProxyTarget(Object object) {
try {
return Hibernate.unproxy(object);
@@ -200,9 +228,15 @@ public class ProxyUtil {
}
/**
* Resolve matching member on target object.
*
* @param proxyMember the proxy member
* @param target the target object
* @return matching member on target object if one exists, otherwise the same member
* @deprecated since 7.1, use {@link #resolveTargetMember(Member, Class)} instead.
* Since 7.2, inject {@link ProxyService} instead.
*/
@Deprecated
@Deprecated(since = "7.1")
public static Member resolveTargetMember(Member proxyMember, Object target) {
return resolveTargetMember(proxyMember, target.getClass());
}
@@ -0,0 +1,194 @@
/*
* 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.util;
import org.apache.commons.lang3.reflect.ConstructorUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.commons.lang3.reflect.MethodUtils;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.ognl.OgnlCache;
import org.apache.struts2.ognl.ProxyCacheFactory;
import org.hibernate.Hibernate;
import org.hibernate.proxy.HibernateProxy;
import org.springframework.aop.TargetClassAware;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.aop.support.AopUtils;
import org.springframework.aop.SpringProxy;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import static java.lang.reflect.Modifier.isPublic;
import static java.lang.reflect.Modifier.isStatic;
/**
* Default implementation of {@link ProxyService}.
* Provides proxy detection and resolution for Spring AOP and Hibernate proxies.
*
* @since 7.2.0
*/
public class StrutsProxyService implements ProxyService {
private final OgnlCache<Class<?>, Boolean> isProxyCache;
private final OgnlCache<Member, Boolean> isProxyMemberCache;
@Inject
@SuppressWarnings("unchecked")
public StrutsProxyService(ProxyCacheFactory<?, ?> proxyCacheFactory) {
this.isProxyCache = (OgnlCache<Class<?>, Boolean>) proxyCacheFactory.buildOgnlCache();
this.isProxyMemberCache = (OgnlCache<Member, Boolean>) proxyCacheFactory.buildOgnlCache();
}
@Override
public Class<?> ultimateTargetClass(Object candidate) {
Class<?> result = null;
if (isSpringAopProxy(candidate)) {
result = springUltimateTargetClass(candidate);
} else if (isHibernateProxy(candidate)) {
result = getHibernateProxyTarget(candidate).getClass();
}
if (result == null) {
result = candidate.getClass();
}
return result;
}
@Override
public boolean isProxy(Object object) {
if (object == null) return false;
return isProxyCache.computeIfAbsent(object.getClass(),
k -> isSpringAopProxy(object) || isHibernateProxy(object));
}
@Override
public boolean isProxyMember(Member member, Object object) {
if (!isStatic(member.getModifiers()) && !isProxy(object)) {
return false;
}
return isProxyMemberCache.computeIfAbsent(member,
k -> isSpringProxyMember(member) || isHibernateProxyMember(member));
}
@Override
public boolean isHibernateProxy(Object object) {
try {
return object != null && HibernateProxy.class.isAssignableFrom(object.getClass());
} catch (LinkageError ignored) {
return false;
}
}
@Override
public boolean isHibernateProxyMember(Member member) {
try {
return hasMember(HibernateProxy.class, member);
} catch (LinkageError ignored) {
return false;
}
}
@Override
public Object getHibernateProxyTarget(Object object) {
try {
return Hibernate.unproxy(object);
} catch (LinkageError ignored) {
return object;
}
}
@Override
public Member resolveTargetMember(Member proxyMember, Class<?> targetClass) {
int mod = proxyMember.getModifiers();
if (proxyMember instanceof Method) {
if (isPublic(mod)) {
return MethodUtils.getMatchingAccessibleMethod(targetClass, proxyMember.getName(), ((Method) proxyMember).getParameterTypes());
} else {
return MethodUtils.getMatchingMethod(targetClass, proxyMember.getName(), ((Method) proxyMember).getParameterTypes());
}
} else if (proxyMember instanceof Field) {
return FieldUtils.getField(targetClass, proxyMember.getName(), isPublic(mod));
} else if (proxyMember instanceof Constructor && isPublic(mod)) {
return ConstructorUtils.getMatchingAccessibleConstructor(targetClass, ((Constructor<?>) proxyMember).getParameterTypes());
}
return proxyMember;
}
@Override
@Deprecated
public Member resolveTargetMember(Member proxyMember, Object target) {
return resolveTargetMember(proxyMember, target.getClass());
}
/**
* Determine the ultimate target class of the given spring bean instance.
*/
private Class<?> springUltimateTargetClass(Object candidate) {
try {
return AopProxyUtils.ultimateTargetClass(candidate);
} catch (LinkageError ignored) {
return candidate.getClass();
}
}
/**
* Check whether the given object is a Spring proxy.
*/
private boolean isSpringAopProxy(Object object) {
try {
return AopUtils.isAopProxy(object);
} catch (LinkageError ignored) {
return false;
}
}
/**
* Check whether the given member is a member of a spring proxy.
*/
private boolean isSpringProxyMember(Member member) {
try {
if (hasMember(Advised.class, member))
return true;
if (hasMember(TargetClassAware.class, member))
return true;
if (hasMember(SpringProxy.class, member))
return true;
} catch (LinkageError ignored) {
}
return false;
}
/**
* Check whether the given class has a given member.
*/
private boolean hasMember(Class<?> clazz, Member member) {
if (member instanceof Method method) {
return null != MethodUtils.getMatchingMethod(clazz, member.getName(), method.getParameterTypes());
}
if (member instanceof Field) {
return null != FieldUtils.getField(clazz, member.getName(), true);
}
if (member instanceof Constructor<?> constructor) {
return null != ConstructorUtils.getMatchingAccessibleConstructor(clazz, constructor.getParameterTypes());
}
return false;
}
}
@@ -283,6 +283,18 @@ struts.ognl.beanInfoCacheType=wtlfu
### application-specific needs.
struts.ognl.beanInfoCacheMaxSize=10000
### Specifies the type of cache to use for proxy detection. See StrutsConstants class for further information.
struts.proxy.cacheType=wtlfu
### Specifies the maximum cache size for proxy detection caches.
struts.proxy.cacheMaxSize=10000
### Specifies the ProxyCacheFactory implementation class.
struts.proxy.cacheFactory=struts
### Specifies the ProxyService implementation class.
struts.proxyService=struts
### Indicates if Dispatcher should handle unexpected exceptions by calling sendError()
### or simply rethrow it as a ServletException to allow future processing by other frameworks like Spring Security
struts.handle.exception=true
+4
View File
@@ -240,6 +240,10 @@
class="org.apache.struts2.ognl.DefaultOgnlExpressionCacheFactory" scope="singleton"/>
<bean type="org.apache.struts2.ognl.BeanInfoCacheFactory" name="struts"
class="org.apache.struts2.ognl.DefaultOgnlBeanInfoCacheFactory" scope="singleton"/>
<bean type="org.apache.struts2.ognl.ProxyCacheFactory" name="struts"
class="org.apache.struts2.ognl.StrutsProxyCacheFactory" scope="singleton"/>
<bean type="org.apache.struts2.util.ProxyService" name="struts"
class="org.apache.struts2.util.StrutsProxyService" scope="singleton"/>
<bean type="org.apache.struts2.url.QueryStringBuilder" name="strutsQueryStringBuilder"
class="org.apache.struts2.url.StrutsQueryStringBuilder" scope="singleton"/>
@@ -30,7 +30,9 @@ import org.apache.struts2.ognl.DefaultOgnlBeanInfoCacheFactory;
import org.apache.struts2.ognl.DefaultOgnlExpressionCacheFactory;
import org.apache.struts2.ognl.OgnlUtil;
import org.apache.struts2.ognl.StrutsOgnlGuard;
import org.apache.struts2.ognl.StrutsProxyCacheFactory;
import org.apache.struts2.ognl.ThreadAllowlist;
import org.apache.struts2.util.StrutsProxyService;
import org.apache.struts2.security.AcceptedPatternsChecker.IsAccepted;
import org.apache.struts2.security.ExcludedPatternsChecker.IsExcluded;
import org.apache.struts2.security.NotExcludedAcceptedPatternsChecker;
@@ -71,6 +73,9 @@ public class StrutsParameterAnnotationTest {
new StrutsOgnlGuard());
parametersInterceptor.setOgnlUtil(ognlUtil);
var proxyService = new StrutsProxyService(new StrutsProxyCacheFactory<>("1000", "basic"));
parametersInterceptor.setProxyService(proxyService);
NotExcludedAcceptedPatternsChecker checker = mock(NotExcludedAcceptedPatternsChecker.class);
when(checker.isAccepted(anyString())).thenReturn(IsAccepted.yes(""));
when(checker.isExcluded(anyString())).thenReturn(IsExcluded.no(Set.of()));
@@ -24,7 +24,9 @@ import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.struts2.TestBean;
import org.apache.struts2.config.ConfigurationException;
import org.apache.struts2.test.TestBean2;
import org.apache.struts2.util.StrutsProxyService;
import org.apache.struts2.util.Foo;
import org.apache.struts2.util.ProxyService;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.LazyInitializer;
import org.junit.Before;
@@ -58,6 +60,7 @@ public class SecurityMemberAccessTest {
protected SecurityMemberAccess sma;
protected ProviderAllowlist mockedProviderAllowlist;
protected ThreadAllowlist mockedThreadAllowlist;
protected ProxyService proxyService;
@Before
public void setUp() {
@@ -65,6 +68,7 @@ public class SecurityMemberAccessTest {
target = new FooBar();
mockedProviderAllowlist = mock(ProviderAllowlist.class);
mockedThreadAllowlist = mock(ThreadAllowlist.class);
proxyService = new StrutsProxyService(new StrutsProxyCacheFactory<>("1000", "basic"));
assignNewSma(true);
}
@@ -77,6 +81,7 @@ public class SecurityMemberAccessTest {
protected void assignNewSmaHelper() {
sma = new SecurityMemberAccess(mockedProviderAllowlist, mockedThreadAllowlist);
sma.setProxyService(proxyService);
}
private <T> T reflectField(String fieldName) throws IllegalAccessException {
@@ -0,0 +1,85 @@
/*
* 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 org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link StrutsProxyCacheFactory}.
*/
public class StrutsProxyCacheFactoryTest {
@Test
public void testCreateBasicCache() {
StrutsProxyCacheFactory<String, Boolean> factory = new StrutsProxyCacheFactory<>("1000", "basic");
OgnlCache<String, Boolean> cache = factory.buildOgnlCache();
assertThat(cache).isNotNull();
assertThat(cache).isInstanceOf(OgnlDefaultCache.class);
assertThat(cache.getEvictionLimit()).isEqualTo(1000);
}
@Test
public void testCreateLruCache() {
StrutsProxyCacheFactory<String, Boolean> factory = new StrutsProxyCacheFactory<>("500", "lru");
OgnlCache<String, Boolean> cache = factory.buildOgnlCache();
assertThat(cache).isNotNull();
assertThat(cache).isInstanceOf(OgnlLRUCache.class);
assertThat(cache.getEvictionLimit()).isEqualTo(500);
}
@Test
public void testCreateWtlfuCache() {
StrutsProxyCacheFactory<String, Boolean> factory = new StrutsProxyCacheFactory<>("2000", "wtlfu");
OgnlCache<String, Boolean> cache = factory.buildOgnlCache();
assertThat(cache).isNotNull();
assertThat(cache).isInstanceOf(OgnlCaffeineCache.class);
assertThat(cache.getEvictionLimit()).isEqualTo(2000);
}
@Test
public void testCacheTypeIgnoresCase() {
StrutsProxyCacheFactory<String, Boolean> factory = new StrutsProxyCacheFactory<>("1000", "BASIC");
OgnlCache<String, Boolean> cache = factory.buildOgnlCache();
assertThat(cache).isInstanceOf(OgnlDefaultCache.class);
}
@Test
public void testGetCacheMaxSize() {
StrutsProxyCacheFactory<String, Boolean> factory = new StrutsProxyCacheFactory<>("5000", "basic");
assertThat(factory.getCacheMaxSize()).isEqualTo(5000);
}
@Test
public void testGetDefaultCacheType() {
StrutsProxyCacheFactory<String, Boolean> factory = new StrutsProxyCacheFactory<>("1000", "lru");
assertThat(factory.getDefaultCacheType()).isEqualTo(OgnlCacheFactory.CacheType.LRU);
}
}
@@ -0,0 +1,275 @@
/*
* 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.util;
import org.apache.struts2.ognl.StrutsProxyCacheFactory;
import org.junit.Before;
import org.junit.Test;
import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.SpringProxy;
import java.lang.reflect.Method;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link StrutsProxyService} with Spring AOP proxies.
* These tests verify the proxy service correctly handles various Spring proxy scenarios.
*/
public class StrutsProxyServiceSpringIntegrationTest {
private StrutsProxyService proxyService;
@Before
public void setUp() {
StrutsProxyCacheFactory<?, ?> factory = new StrutsProxyCacheFactory<>("1000", "basic");
proxyService = new StrutsProxyService(factory);
}
@Test
public void testJdkDynamicProxyIsDetectedAsProxy() {
SimpleService proxy = createJdkDynamicProxy(new SimpleServiceImpl());
assertThat(proxyService.isProxy(proxy)).isTrue();
assertThat(proxy).isInstanceOf(SpringProxy.class);
assertThat(proxy).isInstanceOf(Advised.class);
}
@Test
public void testJdkDynamicProxyUltimateTargetClass() {
SimpleService proxy = createJdkDynamicProxy(new SimpleServiceImpl());
Class<?> targetClass = proxyService.ultimateTargetClass(proxy);
assertThat(targetClass).isEqualTo(SimpleServiceImpl.class);
}
@Test
public void testJdkDynamicProxyMemberDetection() throws NoSuchMethodException {
SimpleService proxy = createJdkDynamicProxy(new SimpleServiceImpl());
// Advised interface method should be detected as proxy member
Method isExposeProxy = proxy.getClass().getMethod("isExposeProxy");
assertThat(proxyService.isProxyMember(isExposeProxy, proxy)).isTrue();
// Business method should not be detected as proxy member
Method getValue = proxy.getClass().getMethod("getValue");
assertThat(proxyService.isProxyMember(getValue, proxy)).isFalse();
}
@Test
public void testJdkDynamicProxyResolveTargetMember() throws NoSuchMethodException {
SimpleService proxy = createJdkDynamicProxy(new SimpleServiceImpl());
Method proxyMethod = proxy.getClass().getMethod("getValue");
// Resolve the method to the target class
Class<?> targetClass = proxyService.ultimateTargetClass(proxy);
var resolved = proxyService.resolveTargetMember(proxyMethod, targetClass);
assertThat(resolved).isNotNull();
assertThat(resolved.getName()).isEqualTo("getValue");
assertThat(resolved.getDeclaringClass()).isEqualTo(SimpleServiceImpl.class);
}
@Test
public void testCglibProxyIsDetectedAsProxy() {
SimpleServiceImpl proxy = createCglibProxy(new SimpleServiceImpl());
assertThat(proxyService.isProxy(proxy)).isTrue();
}
@Test
public void testCglibProxyUltimateTargetClass() {
SimpleServiceImpl proxy = createCglibProxy(new SimpleServiceImpl());
Class<?> targetClass = proxyService.ultimateTargetClass(proxy);
assertThat(targetClass).isEqualTo(SimpleServiceImpl.class);
}
@Test
public void testCglibProxyMemberDetection() throws NoSuchMethodException {
SimpleServiceImpl proxy = createCglibProxy(new SimpleServiceImpl());
// Advised interface method should be detected as proxy member
Method isExposeProxy = proxy.getClass().getMethod("isExposeProxy");
assertThat(proxyService.isProxyMember(isExposeProxy, proxy)).isTrue();
// Business method should not be detected as proxy member
Method getValue = proxy.getClass().getMethod("getValue");
assertThat(proxyService.isProxyMember(getValue, proxy)).isFalse();
}
@Test
public void testCglibProxyResolveTargetMember() throws NoSuchMethodException {
SimpleServiceImpl proxy = createCglibProxy(new SimpleServiceImpl());
Method proxyMethod = proxy.getClass().getMethod("getValue");
// Resolve the method to the target class
Class<?> targetClass = proxyService.ultimateTargetClass(proxy);
var resolved = proxyService.resolveTargetMember(proxyMethod, targetClass);
assertThat(resolved).isNotNull();
assertThat(resolved.getName()).isEqualTo("getValue");
assertThat(resolved.getDeclaringClass()).isEqualTo(SimpleServiceImpl.class);
}
@Test
public void testNestedProxyIsDetectedAsProxy() {
SimpleService innerProxy = createJdkDynamicProxy(new SimpleServiceImpl());
SimpleService outerProxy = createJdkDynamicProxy(innerProxy);
assertThat(proxyService.isProxy(outerProxy)).isTrue();
}
@Test
public void testNestedProxyUltimateTargetClass() {
SimpleService innerProxy = createJdkDynamicProxy(new SimpleServiceImpl());
SimpleService outerProxy = createJdkDynamicProxy(innerProxy);
Class<?> targetClass = proxyService.ultimateTargetClass(outerProxy);
// Should resolve through all proxy layers to the ultimate target
assertThat(targetClass).isEqualTo(SimpleServiceImpl.class);
}
@Test
public void testProxyWithMultipleInterfacesIsDetectedAsProxy() {
MultiInterfaceServiceImpl target = new MultiInterfaceServiceImpl();
Object proxy = createProxyWithMultipleInterfaces(target);
assertThat(proxyService.isProxy(proxy)).isTrue();
}
@Test
public void testProxyWithMultipleInterfacesUltimateTargetClass() {
MultiInterfaceServiceImpl target = new MultiInterfaceServiceImpl();
Object proxy = createProxyWithMultipleInterfaces(target);
Class<?> targetClass = proxyService.ultimateTargetClass(proxy);
assertThat(targetClass).isEqualTo(MultiInterfaceServiceImpl.class);
}
@Test
public void testProxyWithMultipleInterfacesMemberResolution() throws NoSuchMethodException {
MultiInterfaceServiceImpl target = new MultiInterfaceServiceImpl();
Object proxy = createProxyWithMultipleInterfaces(target);
// Get method from FirstInterface
Method getFirst = proxy.getClass().getMethod("getFirst");
Class<?> targetClass = proxyService.ultimateTargetClass(proxy);
var resolved = proxyService.resolveTargetMember(getFirst, targetClass);
assertThat(resolved).isNotNull();
assertThat(resolved.getName()).isEqualTo("getFirst");
assertThat(resolved.getDeclaringClass()).isEqualTo(MultiInterfaceServiceImpl.class);
// Get method from SecondInterface
Method getSecond = proxy.getClass().getMethod("getSecond");
var resolvedSecond = proxyService.resolveTargetMember(getSecond, targetClass);
assertThat(resolvedSecond).isNotNull();
assertThat(resolvedSecond.getName()).isEqualTo("getSecond");
}
@Test
public void testNonProxyObjectNotDetectedAsProxy() {
SimpleServiceImpl nonProxy = new SimpleServiceImpl();
assertThat(proxyService.isProxy(nonProxy)).isFalse();
}
@Test
public void testNonProxyObjectUltimateTargetClass() {
SimpleServiceImpl nonProxy = new SimpleServiceImpl();
Class<?> targetClass = proxyService.ultimateTargetClass(nonProxy);
assertThat(targetClass).isEqualTo(SimpleServiceImpl.class);
}
@Test
public void testNonProxyObjectMemberNotDetectedAsProxyMember() throws NoSuchMethodException {
SimpleServiceImpl nonProxy = new SimpleServiceImpl();
Method getValue = SimpleServiceImpl.class.getMethod("getValue");
assertThat(proxyService.isProxyMember(getValue, nonProxy)).isFalse();
}
private SimpleService createJdkDynamicProxy(SimpleService target) {
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.addAdvice(createNoOpAdvice());
return (SimpleService) proxyFactory.getProxy();
}
private SimpleServiceImpl createCglibProxy(SimpleServiceImpl target) {
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.setProxyTargetClass(true);
proxyFactory.addAdvice(createNoOpAdvice());
return (SimpleServiceImpl) proxyFactory.getProxy();
}
private Object createProxyWithMultipleInterfaces(MultiInterfaceServiceImpl target) {
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.addInterface(FirstInterface.class);
proxyFactory.addInterface(SecondInterface.class);
proxyFactory.addAdvice(createNoOpAdvice());
return proxyFactory.getProxy();
}
private MethodBeforeAdvice createNoOpAdvice() {
return (method, args, target) -> {
// No-op advice for testing
};
}
public interface SimpleService {
String getValue();
}
public static class SimpleServiceImpl implements SimpleService {
@Override
public String getValue() {
return "value";
}
}
public interface FirstInterface {
String getFirst();
}
public interface SecondInterface {
String getSecond();
}
public static class MultiInterfaceServiceImpl implements FirstInterface, SecondInterface {
@Override
public String getFirst() {
return "first";
}
@Override
public String getSecond() {
return "second";
}
}
}
@@ -0,0 +1,399 @@
/*
* 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.util;
import org.apache.struts2.ognl.StrutsProxyCacheFactory;
import org.junit.Before;
import org.junit.Test;
import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.ProxyFactory;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link StrutsProxyService}.
*/
public class StrutsProxyServiceTest {
private StrutsProxyService proxyService;
@Before
public void setUp() {
StrutsProxyCacheFactory<?, ?> factory = new StrutsProxyCacheFactory<>("1000", "basic");
proxyService = new StrutsProxyService(factory);
}
@Test
public void isProxyWithNull() {
assertThat(proxyService.isProxy(null)).isFalse();
}
@Test
public void isProxyWithRegularObject() {
Object regularObject = new Object();
assertThat(proxyService.isProxy(regularObject)).isFalse();
}
@Test
public void isProxyWithString() {
String str = "test";
assertThat(proxyService.isProxy(str)).isFalse();
}
@Test
public void isProxyWithSpringAopProxy() {
TestService proxy = createSpringProxy(new TestServiceImpl());
assertThat(proxyService.isProxy(proxy)).isTrue();
}
@Test
public void isProxyWithSpringCglibProxy() {
TestServiceImpl proxy = createSpringCglibProxy(new TestServiceImpl());
assertThat(proxyService.isProxy(proxy)).isTrue();
}
@Test
public void isProxyCachesResultByClass() {
Object obj1 = new TestServiceImpl();
Object obj2 = new TestServiceImpl();
// First call should populate cache
boolean result1 = proxyService.isProxy(obj1);
// Second call with same class should use cached result
boolean result2 = proxyService.isProxy(obj2);
assertThat(result1).isEqualTo(result2);
assertThat(result1).isFalse();
}
@Test
public void ultimateTargetClassWithRegularObject() {
Object regularObject = new Object();
Class<?> targetClass = proxyService.ultimateTargetClass(regularObject);
assertThat(targetClass).isEqualTo(Object.class);
}
@Test
public void ultimateTargetClassWithString() {
String str = "test";
Class<?> targetClass = proxyService.ultimateTargetClass(str);
assertThat(targetClass).isEqualTo(String.class);
}
@Test
public void ultimateTargetClassWithSpringAopProxy() {
TestService proxy = createSpringProxy(new TestServiceImpl());
Class<?> targetClass = proxyService.ultimateTargetClass(proxy);
assertThat(targetClass).isEqualTo(TestServiceImpl.class);
}
@Test
public void ultimateTargetClassWithSpringCglibProxy() {
TestServiceImpl proxy = createSpringCglibProxy(new TestServiceImpl());
Class<?> targetClass = proxyService.ultimateTargetClass(proxy);
assertThat(targetClass).isEqualTo(TestServiceImpl.class);
}
@Test
public void isHibernateProxyWithNull() {
assertThat(proxyService.isHibernateProxy(null)).isFalse();
}
@Test
public void isHibernateProxyWithRegularObject() {
Object regularObject = new Object();
assertThat(proxyService.isHibernateProxy(regularObject)).isFalse();
}
@Test
public void isHibernateProxyWithSpringProxy() {
TestService proxy = createSpringProxy(new TestServiceImpl());
assertThat(proxyService.isHibernateProxy(proxy)).isFalse();
}
@Test
public void isHibernateProxyMemberWithRegularMethod() throws NoSuchMethodException {
Method method = Object.class.getMethod("toString");
assertThat(proxyService.isHibernateProxyMember(method)).isFalse();
}
@Test
public void isHibernateProxyMemberWithTestServiceMethod() throws NoSuchMethodException {
Method method = TestService.class.getMethod("doSomething");
assertThat(proxyService.isHibernateProxyMember(method)).isFalse();
}
@Test
public void getHibernateProxyTargetWithRegularObject() {
Object regularObject = new Object();
Object result = proxyService.getHibernateProxyTarget(regularObject);
assertThat(result).isSameAs(regularObject);
}
@Test
public void getHibernateProxyTargetWithString() {
String str = "test";
Object result = proxyService.getHibernateProxyTarget(str);
assertThat(result).isSameAs(str);
}
@Test
public void isProxyMemberWithNonProxy() throws NoSuchMethodException {
Object regularObject = new Object();
Method method = Object.class.getMethod("toString");
assertThat(proxyService.isProxyMember(method, regularObject)).isFalse();
}
@Test
public void isProxyMemberWithSpringProxyAndAdvisedMember() throws NoSuchMethodException {
TestService proxy = createSpringProxy(new TestServiceImpl());
Method advisedMethod = Advised.class.getMethod("isExposeProxy");
assertThat(proxyService.isProxyMember(advisedMethod, proxy)).isTrue();
}
@Test
public void isProxyMemberWithSpringProxyAndNonProxyMember() throws NoSuchMethodException {
TestService proxy = createSpringProxy(new TestServiceImpl());
Method doSomethingMethod = proxy.getClass().getMethod("doSomething");
assertThat(proxyService.isProxyMember(doSomethingMethod, proxy)).isFalse();
}
@Test
public void isProxyMemberWithStaticMemberOnNonProxy() throws NoSuchMethodException {
Object regularObject = new TestServiceImpl();
Method staticMethod = TestServiceImpl.class.getMethod("staticMethod");
// Static members are checked regardless of proxy status
assertThat(proxyService.isProxyMember(staticMethod, regularObject)).isFalse();
}
@Test
public void isProxyMemberWithNullObject() throws NoSuchMethodException {
Method method = Object.class.getMethod("toString");
assertThat(proxyService.isProxyMember(method, null)).isFalse();
}
@Test
public void isProxyMemberCachesResult() throws NoSuchMethodException {
TestService proxy = createSpringProxy(new TestServiceImpl());
Method advisedMethod = Advised.class.getMethod("isExposeProxy");
// First call should populate cache
boolean result1 = proxyService.isProxyMember(advisedMethod, proxy);
// Second call should use cached result
boolean result2 = proxyService.isProxyMember(advisedMethod, proxy);
assertThat(result1).isEqualTo(result2);
assertThat(result1).isTrue();
}
@Test
public void resolveTargetMemberReturnsMethodOnTargetClass() throws NoSuchMethodException {
Method toStringMethod = Object.class.getMethod("toString");
Member resolved = proxyService.resolveTargetMember(toStringMethod, String.class);
assertThat(resolved).isNotNull();
assertThat(resolved.getName()).isEqualTo("toString");
assertThat(resolved.getDeclaringClass()).isEqualTo(String.class);
}
@Test
public void resolveTargetMemberDeprecatedMethod() throws NoSuchMethodException {
Method toStringMethod = Object.class.getMethod("toString");
String target = "test";
@SuppressWarnings("deprecation")
Member resolved = proxyService.resolveTargetMember(toStringMethod, target);
assertThat(resolved).isNotNull();
assertThat(resolved.getName()).isEqualTo("toString");
}
@Test
public void resolveTargetMemberWithPrivateMethod() throws NoSuchMethodException {
Method privateMethod = TestServiceImpl.class.getDeclaredMethod("privateMethod");
Member resolved = proxyService.resolveTargetMember(privateMethod, TestServiceImpl.class);
assertThat(resolved).isNotNull();
assertThat(resolved.getName()).isEqualTo("privateMethod");
}
@Test
public void resolveTargetMemberWithMethodNotFoundReturnsNull() throws NoSuchMethodException {
Method charAtMethod = String.class.getMethod("charAt", int.class);
Member resolved = proxyService.resolveTargetMember(charAtMethod, Object.class);
// Method doesn't exist on Object.class, should return null
assertThat(resolved).isNull();
}
@Test
public void resolveTargetMemberWithOverloadedMethod() throws NoSuchMethodException {
Method valueOfInt = String.class.getMethod("valueOf", int.class);
Member resolved = proxyService.resolveTargetMember(valueOfInt, String.class);
assertThat(resolved).isNotNull();
assertThat(resolved.getName()).isEqualTo("valueOf");
assertThat(((Method) resolved).getParameterTypes()).containsExactly(int.class);
}
@Test
public void resolveTargetMemberWithPublicField() throws NoSuchFieldException {
Field publicField = TestBeanWithFields.class.getField("publicField");
Member resolved = proxyService.resolveTargetMember(publicField, TestBeanWithFields.class);
assertThat(resolved).isNotNull();
assertThat(resolved.getName()).isEqualTo("publicField");
assertThat(resolved).isInstanceOf(Field.class);
}
@Test
public void resolveTargetMemberWithPrivateFieldReturnsNull() throws NoSuchFieldException {
Field privateField = TestBeanWithFields.class.getDeclaredField("privateField");
Member resolved = proxyService.resolveTargetMember(privateField, TestBeanWithFields.class);
// Current implementation: non-public fields use forceAccess=false, so they are not found
// This returns null because FieldUtils.getField with forceAccess=false only finds public fields
assertThat(resolved).isNull();
}
@Test
public void resolveTargetMemberWithProtectedFieldReturnsNull() throws NoSuchFieldException {
Field protectedField = TestBeanWithFields.class.getDeclaredField("protectedField");
Member resolved = proxyService.resolveTargetMember(protectedField, TestBeanWithFields.class);
// Current implementation: non-public fields use forceAccess=false, so they are not found
// This returns null because FieldUtils.getField with forceAccess=false only finds public fields
assertThat(resolved).isNull();
}
@Test
public void resolveTargetMemberWithFieldNotFoundReturnsNull() throws NoSuchFieldException {
Field publicField = TestBeanWithFields.class.getField("publicField");
Member resolved = proxyService.resolveTargetMember(publicField, Object.class);
// Field doesn't exist on Object.class
assertThat(resolved).isNull();
}
@Test
public void resolveTargetMemberWithDefaultConstructor() throws NoSuchMethodException {
Constructor<?> constructor = TestServiceImpl.class.getConstructor();
Member resolved = proxyService.resolveTargetMember(constructor, TestServiceImpl.class);
assertThat(resolved).isNotNull();
assertThat(resolved).isInstanceOf(Constructor.class);
}
@Test
public void resolveTargetMemberWithParameterizedConstructor() throws NoSuchMethodException {
Constructor<?> constructor = TestBeanWithConstructor.class.getConstructor(String.class, int.class);
Member resolved = proxyService.resolveTargetMember(constructor, TestBeanWithConstructor.class);
assertThat(resolved).isNotNull();
assertThat(resolved).isInstanceOf(Constructor.class);
assertThat(((Constructor<?>) resolved).getParameterTypes()).containsExactly(String.class, int.class);
}
@Test
public void resolveTargetMemberWithConstructorNotFoundReturnsNull() throws NoSuchMethodException {
Constructor<?> constructor = TestBeanWithConstructor.class.getConstructor(String.class, int.class);
Member resolved = proxyService.resolveTargetMember(constructor, TestServiceImpl.class);
// Constructor with those params doesn't exist on TestServiceImpl, returns null
assertThat(resolved).isNull();
}
@Test
public void resolveTargetMemberWithPrivateConstructorReturnsOriginal() throws NoSuchMethodException {
Constructor<?> privateConstructor = TestBeanWithPrivateConstructor.class.getDeclaredConstructor(String.class);
Member resolved = proxyService.resolveTargetMember(privateConstructor, TestBeanWithPrivateConstructor.class);
// Private constructor is not accessible, returns original
assertThat(resolved).isSameAs(privateConstructor);
}
private TestService createSpringProxy(TestService target) {
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.addAdvice((MethodBeforeAdvice) (method, args, t) -> {
// No-op advice
});
return (TestService) proxyFactory.getProxy();
}
private TestServiceImpl createSpringCglibProxy(TestServiceImpl target) {
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.setProxyTargetClass(true);
proxyFactory.addAdvice((MethodBeforeAdvice) (method, args, t) -> {
// No-op advice
});
return (TestServiceImpl) proxyFactory.getProxy();
}
public interface TestService {
void doSomething();
}
public static class TestServiceImpl implements TestService {
@Override
public void doSomething() {
// No-op
}
public static void staticMethod() {
// Static method for testing
}
private void privateMethod() {
// Private method for testing
}
}
public static class TestBeanWithFields {
public String publicField;
protected String protectedField;
private String privateField;
String packagePrivateField;
}
public static class TestBeanWithConstructor {
private final String name;
private final int value;
public TestBeanWithConstructor(String name, int value) {
this.name = name;
this.value = value;
}
}
public static class TestBeanWithPrivateConstructor {
private TestBeanWithPrivateConstructor(String value) {
// Private constructor
}
public TestBeanWithPrivateConstructor() {
// Public default constructor
}
}
}
@@ -29,5 +29,6 @@ public class ExternalSecurityMemberAccessTest extends SecurityMemberAccessTest {
@Override
protected void assignNewSmaHelper() {
sma = new ExternalSecurityMemberAccess(mockedProviderAllowlist, mockedThreadAllowlist);
sma.setProxyService(proxyService);
}
}