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 31dc7f667aece368c0f9f8c03a81de8b3fcaa22b Author: Lukasz Lenart <[email protected]> AuthorDate: Mon Jul 27 10:33:34 2026 +0200 WW-5659 docs(core): correct isUnresolved javadoc and pin empty-value fail-closed behavior isUnresolved cannot distinguish a failed ${...} resolution from an expression that legitimately evaluates to an empty string; the parser gives no other signal. The previous javadoc wrongly claimed the raw template let it tell the two apart. Fix the javadoc to state the actual, intentional rule (fail-closed: treat both as unusable), and add a test pinning that a legitimately-empty expression is treated as unresolved rather than written. --- .../apache/struts2/interceptor/WithLazyParams.java | 21 +++++++++++++++------ .../struts2/interceptor/LazyParamInjectorTest.java | 14 ++++++++++++++ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/org/apache/struts2/interceptor/WithLazyParams.java b/core/src/main/java/org/apache/struts2/interceptor/WithLazyParams.java index 8c74b398d..f683ebb7c 100644 --- a/core/src/main/java/org/apache/struts2/interceptor/WithLazyParams.java +++ b/core/src/main/java/org/apache/struts2/interceptor/WithLazyParams.java @@ -91,9 +91,13 @@ public interface WithLazyParams { /** * Resolves configured params into a per-invocation holder, leaving the interceptor untouched. * <p> - * A {@code ${...}} expression that cannot be resolved is not written: the holder keeps its - * seeded configuration value and is notified via {@link InterceptorParams#unresolved(String)}, - * so a broken expression cannot silently relax a validation policy. + * A {@code ${...}} expression that resolves to null or an empty value is not written: the + * holder keeps its seeded configuration value and is notified via + * {@link InterceptorParams#unresolved(String)}. This also catches an expression that + * legitimately evaluates to an empty string, which is indistinguishable from a failed + * resolution (see {@link #isUnresolved}); for a fail-closed policy such as an allowlist, + * treating both as unusable is the safe reading, so a broken expression cannot silently + * relax a validation policy. * * @since 7.3.0 */ @@ -122,9 +126,14 @@ public interface WithLazyParams { } /** - * {@link org.apache.struts2.util.OgnlTextParser} yields an empty string for an expression that - * does not resolve and gives no other signal, so the raw template is needed to tell that apart - * from a legitimately empty value. + * A {@code ${...}} param is treated as unresolved when its evaluated value is null or empty. + * <p> + * {@link org.apache.struts2.util.OgnlTextParser} yields the same empty string both when an + * expression fails to resolve and when it resolves to a legitimately empty value — there is + * no way to tell the two apart from the parser's output alone. This method does not attempt + * to; a param that legitimately evaluates to an empty string is therefore also reported as + * unresolved. That is a deliberate fail-closed choice: for a security-sensitive param (e.g. + * an allowlist), silently accepting an unintended empty value is worse than refusing it. */ private boolean isUnresolved(String rawValue, Object paramValue) { return rawValue != null diff --git a/core/src/test/java/org/apache/struts2/interceptor/LazyParamInjectorTest.java b/core/src/test/java/org/apache/struts2/interceptor/LazyParamInjectorTest.java index 8939c1313..46f5282f0 100644 --- a/core/src/test/java/org/apache/struts2/interceptor/LazyParamInjectorTest.java +++ b/core/src/test/java/org/apache/struts2/interceptor/LazyParamInjectorTest.java @@ -50,6 +50,7 @@ public class LazyParamInjectorTest extends StrutsInternalTestCase { public static class Bean { public String getLabel() { return "resolved-label"; } public Long getLimit() { return 4096L; } + public String getBlank() { return ""; } } private ActionContext context; @@ -113,6 +114,19 @@ public class LazyParamInjectorTest extends StrutsInternalTestCase { assertThat(holder.getUnresolvedCalls()).containsExactly("name"); } + public void testExpressionResolvingToEmptyIsTreatedAsUnresolved() { + Holder seeded = new Holder(); + seeded.setName("seeded-value"); + + Map<String, String> params = new HashMap<>(); + params.put("name", "${blank}"); + + Holder holder = injector.resolveInto(seeded, params, context); + + assertThat(holder.getName()).isEqualTo("seeded-value"); + assertThat(holder.getUnresolvedCalls()).containsExactly("name"); + } + public void testResolvesDisabledOntoDisableParams() { Map<String, String> params = new HashMap<>(); params.put("disabled", "true");
