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");

Reply via email to