This is an automated email from the ASF dual-hosted git repository.

kusal pushed a commit to branch WW-5534-annotation-allowlist-proxy
in repository https://gitbox.apache.org/repos/asf/struts.git

commit ccd06612abde48e90995d166881829f2c24bd51f
Author: Kusal Kithul-Godage <g...@kusal.io>
AuthorDate: Fri Feb 28 04:42:36 2025 +1100

    WW-5534 Allow @StrutsParameter recognition and OGNL allowlist for Spring 
proxies
---
 .../parameter/ParametersInterceptor.java           | 34 +++++++++++++++------
 .../apache/struts2/ognl/SecurityMemberAccess.java  | 35 +++++++---------------
 2 files changed, 36 insertions(+), 33 deletions(-)

diff --git 
a/core/src/main/java/org/apache/struts2/interceptor/parameter/ParametersInterceptor.java
 
b/core/src/main/java/org/apache/struts2/interceptor/parameter/ParametersInterceptor.java
index 92449415e..4cd1c3faa 100644
--- 
a/core/src/main/java/org/apache/struts2/interceptor/parameter/ParametersInterceptor.java
+++ 
b/core/src/main/java/org/apache/struts2/interceptor/parameter/ParametersInterceptor.java
@@ -33,12 +33,14 @@ import org.apache.struts2.dispatcher.HttpParameters;
 import org.apache.struts2.dispatcher.Parameter;
 import org.apache.struts2.inject.Inject;
 import org.apache.struts2.interceptor.MethodFilterInterceptor;
+import org.apache.struts2.ognl.OgnlUtil;
 import org.apache.struts2.ognl.ThreadAllowlist;
 import org.apache.struts2.security.AcceptedPatternsChecker;
 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.TextParseUtil;
 import org.apache.struts2.util.ValueStack;
 import org.apache.struts2.util.ValueStackFactory;
@@ -46,7 +48,6 @@ import 
org.apache.struts2.util.reflection.ReflectionContextState;
 
 import java.beans.BeanInfo;
 import java.beans.IntrospectionException;
-import java.beans.Introspector;
 import java.beans.PropertyDescriptor;
 import java.lang.reflect.AnnotatedElement;
 import java.lang.reflect.Field;
@@ -93,6 +94,7 @@ public class ParametersInterceptor extends 
MethodFilterInterceptor {
     protected boolean requireAnnotationsTransitionMode = false;
 
     private ValueStackFactory valueStackFactory;
+    private OgnlUtil ognlUtil;
     protected ThreadAllowlist threadAllowlist;
     private ExcludedPatternsChecker excludedPatterns;
     private AcceptedPatternsChecker acceptedPatterns;
@@ -104,6 +106,11 @@ public class ParametersInterceptor extends 
MethodFilterInterceptor {
         this.valueStackFactory = valueStackFactory;
     }
 
+    @Inject
+    public void setOgnlUtil(OgnlUtil ognlUtil) {
+        this.ognlUtil = ognlUtil;
+    }
+
     @Inject
     public void setThreadAllowlist(ThreadAllowlist threadAllowlist) {
         this.threadAllowlist = threadAllowlist;
@@ -395,6 +402,7 @@ public class ParametersInterceptor extends 
MethodFilterInterceptor {
     }
 
     protected boolean hasValidAnnotatedPropertyDescriptor(Object action, 
PropertyDescriptor propDesc, long paramDepth) {
+        Class<?> actionClass = ultimateClass(action);
         Method relevantMethod = paramDepth == 0 ? propDesc.getWriteMethod() : 
propDesc.getReadMethod();
         if (relevantMethod == null) {
             return false;
@@ -412,7 +420,7 @@ public class ParametersInterceptor extends 
MethodFilterInterceptor {
             return false;
         }
         LOG.debug("Success: Matching annotated method [{}] found for property 
[{}] of depth [{}] on Action [{}]",
-                relevantMethod.getName(), propDesc.getName(), paramDepth, 
action.getClass().getSimpleName());
+                relevantMethod.getName(), propDesc.getName(), paramDepth, 
actionClass.getSimpleName());
         if (paramDepth >= 1) {
             allowlistClass(propDesc.getPropertyType());
         }
@@ -451,24 +459,25 @@ public class ParametersInterceptor extends 
MethodFilterInterceptor {
     }
 
     protected boolean hasValidAnnotatedField(Object action, String fieldName, 
long paramDepth) {
+        Class<?> actionClass = ultimateClass(action);
         LOG.debug("No matching annotated method found for property [{}] of 
depth [{}] on Action [{}], now also checking for public field",
-                fieldName, paramDepth, action.getClass().getSimpleName());
+                fieldName, paramDepth, actionClass.getSimpleName());
         Field field;
         try {
-            field = action.getClass().getDeclaredField(fieldName);
+            field = actionClass.getDeclaredField(fieldName);
         } catch (NoSuchFieldException e) {
-            LOG.debug("Matching field for property [{}] not found on Action 
[{}]", fieldName, action.getClass().getSimpleName());
+            LOG.debug("Matching field for property [{}] not found on Action 
[{}]", fieldName, actionClass.getSimpleName());
             return false;
         }
         if (!Modifier.isPublic(field.getModifiers())) {
-            LOG.debug("Matching field [{}] is not public on Action [{}]", 
field.getName(), action.getClass().getSimpleName());
+            LOG.debug("Matching field [{}] is not public on Action [{}]", 
field.getName(), actionClass.getSimpleName());
             return false;
         }
         if (getPermittedInjectionDepth(field) < paramDepth) {
             String logMessage = format(
                     "Parameter injection for field [%s] on Action [%s] 
rejected. Ensure it is annotated with @StrutsParameter with an appropriate 
'depth'.",
                     field.getName(),
-                    action.getClass().getName());
+                    actionClass.getName());
             if (devMode) {
                 notifyDeveloperOfError(LOG, action, logMessage);
             } else {
@@ -477,7 +486,7 @@ public class ParametersInterceptor extends 
MethodFilterInterceptor {
             return false;
         }
         LOG.debug("Success: Matching annotated public field [{}] found for 
property of depth [{}] on Action [{}]",
-                field.getName(), paramDepth, 
action.getClass().getSimpleName());
+                field.getName(), paramDepth, actionClass.getSimpleName());
         if (paramDepth >= 1) {
             allowlistClass(field.getType());
         }
@@ -510,9 +519,16 @@ public class ParametersInterceptor extends 
MethodFilterInterceptor {
         return element.getAnnotation(StrutsParameter.class);
     }
 
+    protected Class<?> ultimateClass(Object action) {
+        if (ProxyUtil.isProxy(action)) {
+            return ProxyUtil.ultimateTargetClass(action);
+        }
+        return action.getClass();
+    }
+
     protected BeanInfo getBeanInfo(Object action) {
         try {
-            return Introspector.getBeanInfo(action.getClass());
+            return ognlUtil.getBeanInfo(ultimateClass(action));
         } catch (IntrospectionException e) {
             LOG.warn("Error introspecting Action {} for parameter injection 
validation", action.getClass(), e);
             return null;
diff --git 
a/core/src/main/java/org/apache/struts2/ognl/SecurityMemberAccess.java 
b/core/src/main/java/org/apache/struts2/ognl/SecurityMemberAccess.java
index 64a8fa5a4..928dda78d 100644
--- a/core/src/main/java/org/apache/struts2/ognl/SecurityMemberAccess.java
+++ b/core/src/main/java/org/apache/struts2/ognl/SecurityMemberAccess.java
@@ -212,16 +212,17 @@ public class SecurityMemberAccess implements MemberAccess 
{
             return true;
         }
 
+        Class<?> targetClass = target != null ? target.getClass() : null;
+
         if (!disallowProxyObjectAccess && ProxyUtil.isProxy(target)) {
-            // If `disallowProxyObjectAccess` is not set, allow resolving 
Hibernate entities to their underlying
-            // classes/members. This allows the allowlist capability to 
continue working and offer some level of
+            // 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. This is preferred to having to disable the allowlist 
capability entirely.
-            Object newTarget = ProxyUtil.getHibernateProxyTarget(target);
-            if (newTarget != target) {
-                logAllowlistHibernateEntity(target, newTarget);
-                target = newTarget;
-                member = ProxyUtil.resolveTargetMember(member, newTarget);
+            // entities and Spring proxies. This is preferred to having to 
disable the allowlist capability entirely.
+            Class<?> newTargetClass = ProxyUtil.ultimateTargetClass(target);
+            if (newTargetClass != targetClass) {
+                targetClass = newTargetClass;
+                member = ProxyUtil.resolveTargetMember(member, newTargetClass);
             }
         }
 
@@ -231,10 +232,10 @@ public class SecurityMemberAccess implements MemberAccess 
{
                     memberClass, member, STRUTS_ALLOWLIST_CLASSES, 
STRUTS_ALLOWLIST_PACKAGE_NAMES);
             return false;
         }
-        if (target == null || target.getClass() == memberClass) {
+
+        if (targetClass == null || targetClass == memberClass) {
             return true;
         }
-        Class<?> targetClass = target.getClass();
         if (!isClassAllowlisted(targetClass)) {
             LOG.warn("Target class [{}] of target [{}] is not allowlisted! Add 
to '{}' or '{}' configuration.",
                     targetClass, target, STRUTS_ALLOWLIST_CLASSES, 
STRUTS_ALLOWLIST_PACKAGE_NAMES);
@@ -243,20 +244,6 @@ public class SecurityMemberAccess implements MemberAccess {
         return true;
     }
 
-    private void logAllowlistHibernateEntity(Object original, Object resolved) 
{
-        if (!isDevMode && !LOG.isDebugEnabled()) {
-            return;
-        }
-        String msg = "Hibernate entity [{}] resolved to [{}] for purpose of 
OGNL allowlisting." +
-                " We don't recommend executing OGNL expressions against 
Hibernate entities, you may disallow this behaviour using the configuration 
`{}=true`.";
-        Object[] args = {original, resolved, 
StrutsConstants.STRUTS_DISALLOW_PROXY_OBJECT_ACCESS};
-        if (isDevMode) {
-            LOG.warn(msg, args);
-        } else {
-            LOG.debug(msg, args);
-        }
-    }
-
     protected boolean isClassAllowlisted(Class<?> clazz) {
         return allowlistClasses.contains(clazz)
                 || ALLOWLIST_REQUIRED_CLASSES.contains(clazz)

Reply via email to