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; } }
