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

lukaszlenart pushed a commit to branch WW-5631-chaining-require-annotations
in repository https://gitbox.apache.org/repos/asf/struts.git

commit b8dd0365470c742cba9b74de2b6a98b53006144c
Author: Lukasz Lenart <[email protected]>
AuthorDate: Wed May 27 08:18:50 2026 +0200

    WW-5631 feat(chaining): enforce @StrutsParameter on target when opted in
    
    Co-Authored-By: Claude Opus 4.7 <[email protected]>
---
 .../struts2/interceptor/ChainingInterceptor.java   | 71 +++++++++++++++++++++-
 1 file changed, 70 insertions(+), 1 deletion(-)

diff --git 
a/core/src/main/java/org/apache/struts2/interceptor/ChainingInterceptor.java 
b/core/src/main/java/org/apache/struts2/interceptor/ChainingInterceptor.java
index 9c18d8869..c9b8a87c8 100644
--- a/core/src/main/java/org/apache/struts2/interceptor/ChainingInterceptor.java
+++ b/core/src/main/java/org/apache/struts2/interceptor/ChainingInterceptor.java
@@ -24,6 +24,8 @@ import org.apache.struts2.ActionInvocation;
 import org.apache.struts2.StrutsConstants;
 import org.apache.struts2.Unchainable;
 import org.apache.struts2.inject.Inject;
+import org.apache.struts2.interceptor.parameter.ParameterAuthorizer;
+import org.apache.struts2.ognl.OgnlUtil;
 import org.apache.struts2.result.ActionChainResult;
 import org.apache.struts2.result.Result;
 import org.apache.struts2.util.CompoundRoot;
@@ -32,12 +34,16 @@ import org.apache.struts2.util.TextParseUtil;
 import org.apache.struts2.util.ValueStack;
 import org.apache.struts2.util.reflection.ReflectionProvider;
 
+import java.beans.BeanInfo;
+import java.beans.IntrospectionException;
+import java.beans.PropertyDescriptor;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 
 /**
@@ -135,6 +141,9 @@ public class ChainingInterceptor extends 
AbstractInterceptor {
     protected Collection<String> includes;
     protected ReflectionProvider reflectionProvider;
     private ProxyService proxyService;
+    private boolean requireAnnotations = false;
+    private ParameterAuthorizer parameterAuthorizer;
+    private OgnlUtil ognlUtil;
 
     @Inject
     public void setReflectionProvider(ReflectionProvider prov) {
@@ -146,6 +155,21 @@ public class ChainingInterceptor extends 
AbstractInterceptor {
         this.proxyService = proxyService;
     }
 
+    @Inject
+    public void setParameterAuthorizer(ParameterAuthorizer 
parameterAuthorizer) {
+        this.parameterAuthorizer = parameterAuthorizer;
+    }
+
+    @Inject
+    public void setOgnlUtil(OgnlUtil ognlUtil) {
+        this.ognlUtil = ognlUtil;
+    }
+
+    @Inject(value = StrutsConstants.STRUTS_CHAINING_REQUIRE_ANNOTATIONS, 
required = false)
+    public void setRequireAnnotations(String requireAnnotations) {
+        this.requireAnnotations = "true".equalsIgnoreCase(requireAnnotations);
+    }
+
     @Inject(value = StrutsConstants.STRUTS_CHAINING_COPY_ERRORS, required = 
false)
     public void setCopyErrors(String copyErrors) {
         this.copyErrors = "true".equalsIgnoreCase(copyErrors);
@@ -183,7 +207,52 @@ public class ChainingInterceptor extends 
AbstractInterceptor {
             if (proxyService.isProxy(action)) {
                 editable = proxyService.ultimateTargetClass(action);
             }
-            reflectionProvider.copy(object, action, ctxMap, prepareExcludes(), 
includes, editable);
+            Collection<String> copyExcludes = prepareExcludes();
+            if (requireAnnotations) {
+                Class<?> targetClass = editable != null ? editable : 
action.getClass();
+                BeanInfo beanInfo = getTargetBeanInfo(targetClass);
+                if (beanInfo == null) {
+                    // Fail closed: cannot prove which properties are 
annotated, so copy nothing.
+                    LOG.warn("Chaining: unable to introspect target [{}]; 
skipping property copy " +
+                            "(struts.chaining.requireAnnotations enabled)", 
targetClass.getName());
+                    continue;
+                }
+                copyExcludes = excludeUnauthorizedProperties(copyExcludes, 
beanInfo, targetClass, action);
+            }
+            reflectionProvider.copy(object, action, ctxMap, copyExcludes, 
includes, editable);
+        }
+    }
+
+    /**
+     * Returns the excludes to use for the copy: the base excludes unioned 
with the names of all
+     * writable target properties that are not authorized by {@code 
@StrutsParameter}.
+     */
+    private Collection<String> 
excludeUnauthorizedProperties(Collection<String> baseExcludes,
+                                                             BeanInfo 
beanInfo, Class<?> targetClass, Object action) {
+        Set<String> merged = new HashSet<>();
+        if (baseExcludes != null) {
+            merged.addAll(baseExcludes);
+        }
+        for (PropertyDescriptor descriptor : 
beanInfo.getPropertyDescriptors()) {
+            if (descriptor.getWriteMethod() == null) {
+                continue;
+            }
+            String name = descriptor.getName();
+            if (!parameterAuthorizer.isAuthorized(name, action, action)) {
+                LOG.warn("Chaining: property [{}] not copied to [{}] because 
it is not annotated with @StrutsParameter",
+                        name, targetClass.getName());
+                merged.add(name);
+            }
+        }
+        return merged;
+    }
+
+    private BeanInfo getTargetBeanInfo(Class<?> targetClass) {
+        try {
+            return ognlUtil.getBeanInfo(targetClass);
+        } catch (IntrospectionException e) {
+            LOG.warn("Error introspecting Action {} for chaining 
@StrutsParameter enforcement", targetClass, e);
+            return null;
         }
     }
 

Reply via email to