This is an automated email from the ASF dual-hosted git repository. lukaszlenart pushed a commit to branch WW-5659-lazy-params-request-scoping in repository https://gitbox.apache.org/repos/asf/struts.git
commit ac4676db5daf7f453f993331b90dcc3e8c88846b Author: Lukasz Lenart <[email protected]> AuthorDate: Mon Jul 27 11:59:41 2026 +0200 WW-5659 chore(core): harden the policy sets and tidy the lazy params dispatch UploadPolicy handed out the mutable HashSet built by commaDelimitedStringToSet, which the copy constructor shares by reference with the configured policy, so a subclass overriding the protected acceptFile could have rewritten process-wide config from a request thread. The sets are unmodifiable now. Also document that unresolved() records `disabled` like any other param, so an unresolvable disabled expression leaves the interceptor enabled and rejects every upload; and in DefaultActionInvocation use normal imports for the params types, make the interceptor local final, word the three skip logs consistently and identify the interceptor by its mapping name, and note that the name-based param merge is inherited behaviour whose duplicate-ref handling is questionable. Co-Authored-By: Claude Opus 5 <[email protected]> --- .../apache/struts2/DefaultActionInvocation.java | 38 +++++++++++++++++----- .../apache/struts2/interceptor/UploadPolicy.java | 14 +++++++- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/org/apache/struts2/DefaultActionInvocation.java b/core/src/main/java/org/apache/struts2/DefaultActionInvocation.java index 913590410..72a35afe5 100644 --- a/core/src/main/java/org/apache/struts2/DefaultActionInvocation.java +++ b/core/src/main/java/org/apache/struts2/DefaultActionInvocation.java @@ -30,7 +30,9 @@ import org.apache.struts2.config.entities.ResultConfig; import org.apache.struts2.inject.Container; import org.apache.struts2.inject.Inject; import org.apache.struts2.interceptor.ConditionalInterceptor; +import org.apache.struts2.interceptor.DisableParams; import org.apache.struts2.interceptor.Interceptor; +import org.apache.struts2.interceptor.InterceptorParams; import org.apache.struts2.interceptor.PreResultListener; import org.apache.struts2.interceptor.WithLazyParams; import org.apache.struts2.ognl.OgnlUtil; @@ -258,11 +260,11 @@ public class DefaultActionInvocation implements ActionInvocation { if (asyncManager == null || !asyncManager.hasAsyncActionResult()) { if (interceptors.hasNext()) { final InterceptorMapping interceptorMapping = interceptors.next(); - Interceptor interceptor = interceptorMapping.getInterceptor(); + final Interceptor interceptor = interceptorMapping.getInterceptor(); if (interceptor instanceof WithLazyParams<?> lazyInterceptor) { resultCode = invokeWithLazyParams(lazyInterceptor, interceptorMapping); } else if (interceptor instanceof ConditionalInterceptor conditionalInterceptor) { - resultCode = executeConditional(conditionalInterceptor); + resultCode = executeConditional(conditionalInterceptor, interceptorMapping.getName()); } else { LOG.debug("Executing normal interceptor: {}", interceptorMapping.getName()); resultCode = interceptor.intercept(this); @@ -313,18 +315,18 @@ public class DefaultActionInvocation implements ActionInvocation { * lazily resolved {@code disabled} flag and any custom {@code shouldIntercept} must be honoured * here, because the single-argument {@code intercept} is not the entry point on this path. */ - private <P extends org.apache.struts2.interceptor.InterceptorParams> String invokeWithLazyParams( + private <P extends InterceptorParams> String invokeWithLazyParams( WithLazyParams<P> lazyInterceptor, InterceptorMapping interceptorMapping) throws Exception { P lazyParams = lazyParamInjector.resolveInto( lazyInterceptor.newLazyParams(), mergedParams(interceptorMapping), invocationContext); - if (lazyParams instanceof org.apache.struts2.interceptor.DisableParams disableParams && disableParams.isDisabled()) { - LOG.debug("Interceptor: {} is disabled for this invocation, skipping to next", interceptorMapping.getName()); + if (lazyParams instanceof DisableParams disableParams && disableParams.isDisabled()) { + LOG.debug("Interceptor: {} is disabled by its lazily resolved params, skipping to next", interceptorMapping.getName()); return this.invoke(); } if (lazyInterceptor instanceof ConditionalInterceptor conditionalInterceptor && !conditionalInterceptor.shouldIntercept(this)) { - LOG.debug("Interceptor: {} is disabled, skipping to next", interceptorMapping.getName()); + LOG.debug("Interceptor: {} declined by shouldIntercept() on the lazy params path, skipping to next", interceptorMapping.getName()); return this.invoke(); } LOG.debug("Executing lazy params interceptor: {}", interceptorMapping.getName()); @@ -332,6 +334,13 @@ public class DefaultActionInvocation implements ActionInvocation { } /** + * Merges the params declared on the interceptor-ref with those of the mapping being invoked. + * <p> + * The name-based lookup is inherited behaviour, kept as-is: the mapping is normally the very one + * found by name, so the merge is a no-op, and when a stack references the same interceptor name + * twice with different params it merges the first mapping's params over the current one, which + * is questionable. Changing it is out of scope here. + * * @return a fresh map; the mapping's own param map is shared across requests and must not be mutated */ private Map<String, String> mergedParams(InterceptorMapping interceptorMapping) { @@ -343,12 +352,25 @@ public class DefaultActionInvocation implements ActionInvocation { return merged; } + /** + * @deprecated since 7.3.0, use {@link #executeConditional(ConditionalInterceptor, String)} so the + * interceptor is identified by its mapping name in the logs. + */ + @Deprecated protected String executeConditional(ConditionalInterceptor conditionalInterceptor) throws Exception { + return executeConditional(conditionalInterceptor, conditionalInterceptor.getClass().getSimpleName()); + } + + /** + * @param interceptorName the name of the interceptor mapping being invoked, used for logging + * @since 7.3.0 + */ + protected String executeConditional(ConditionalInterceptor conditionalInterceptor, String interceptorName) throws Exception { if (conditionalInterceptor.shouldIntercept(this)) { - LOG.debug("Executing conditional interceptor: {}", conditionalInterceptor.getClass().getSimpleName()); + LOG.debug("Executing conditional interceptor: {}", interceptorName); return conditionalInterceptor.intercept(this); } else { - LOG.debug("Interceptor: {} is disabled, skipping to next", conditionalInterceptor.getClass().getSimpleName()); + LOG.debug("Interceptor: {} declined by shouldIntercept(), skipping to next", interceptorName); return this.invoke(); } } diff --git a/core/src/main/java/org/apache/struts2/interceptor/UploadPolicy.java b/core/src/main/java/org/apache/struts2/interceptor/UploadPolicy.java index 08702dee4..5911a401e 100644 --- a/core/src/main/java/org/apache/struts2/interceptor/UploadPolicy.java +++ b/core/src/main/java/org/apache/struts2/interceptor/UploadPolicy.java @@ -87,6 +87,13 @@ public class UploadPolicy extends DisableParams { * A parameter that could not be resolved makes this policy unusable: the upload is rejected * rather than validated against a partially-resolved policy, so a broken expression cannot * silently relax validation. + * <p> + * <strong>Any</strong> param name is recorded, {@code disabled} included. An unresolvable + * {@code <param name="disabled">${...}</param>} therefore leaves the interceptor enabled (the + * flag keeps its configured value) <em>and</em> marks this policy unusable, so every upload in + * that invocation is rejected. That is deliberate — a broken dispatch flag is a broken + * configuration, and refusing the upload is the safe reading — but it means a typo in + * {@code disabled} takes out upload validation rather than only the disabling. */ @Override public void unresolved(String paramName) { @@ -108,9 +115,14 @@ public class UploadPolicy extends DisableParams { return new UploadPolicy(this); } + /** + * The resulting set is unmodifiable: the copy constructor shares it by reference with the + * configured policy, so every per-invocation policy that does not override the param would + * otherwise hand out a live handle on process-wide configuration. + */ private static Set<String> toSet(String commaDelimited) { return commaDelimited == null ? Collections.emptySet() - : TextParseUtil.commaDelimitedStringToSet(commaDelimited); + : Collections.unmodifiableSet(TextParseUtil.commaDelimitedStringToSet(commaDelimited)); } }
