This is an automated email from the ASF dual-hosted git repository. lukaszlenart pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/struts.git
commit 871b1bd6875e7222ff350913c1328a04f8f0b06e Author: Emil Huseynov <[email protected]> AuthorDate: Sun Nov 1 15:49:47 2026 +0400 WW-5820 Reject class before the model check so a Map model cannot exempt it Review follow-up. The class guard sat after the model's takesParameter check, and a Map-backed model takes any depth-0 name through map.put, so class on a Map model was exempted where the non-ModelDriven path rejects it. Move the class check to the top of isAuthorizedOnModelDrivenAction so it is rejected before the model is consulted, matching main whatever the model is. Pin it with modelDriven_mapModel_classAtDepthZero_rejected. --- .../interceptor/parameter/StrutsParameterAuthorizer.java | 16 +++++++++------- .../interceptor/parameter/ParameterAuthorizerTest.java | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/org/apache/struts2/interceptor/parameter/StrutsParameterAuthorizer.java b/core/src/main/java/org/apache/struts2/interceptor/parameter/StrutsParameterAuthorizer.java index 2d53378bf..93f0936f1 100644 --- a/core/src/main/java/org/apache/struts2/interceptor/parameter/StrutsParameterAuthorizer.java +++ b/core/src/main/java/org/apache/struts2/interceptor/parameter/StrutsParameterAuthorizer.java @@ -175,20 +175,22 @@ public class StrutsParameterAuthorizer implements ParameterAuthorizer { * accessor on the model that introspection here cannot see. That fallback guarantees only that the name reaches * no object this code can model, which is no more permissive than the blanket exemption this scoping replaces. * <p> - * {@code class} is the exception to that fallback: it is invisible to introspection here rather than absent, so - * it is rejected instead of taking the fallback, which keeps a ModelDriven action from handing OGNL a - * {@code class} path that the ordinary non-ModelDriven path would have rejected for want of an annotation. + * {@code class} is rejected up front, before the model is even consulted: it is invisible to introspection here + * rather than absent, so it would otherwise slip through either the custom-accessor fallback or a Map-backed + * model's catch-all (a {@code Map} model takes any depth-0 name). Rejecting it first keeps a ModelDriven action + * from handing OGNL a {@code class} path that the ordinary non-ModelDriven path would have rejected for want of + * an annotation, matching main's behaviour exactly whatever the model is. */ protected boolean isAuthorizedOnModelDrivenAction(String rootProperty, Object model, Object action, long paramDepth) { + if (CLASS_PROPERTY.equals(rootProperty)) { + LOG.debug("Property [class] is not an unknown property but Object.getClass() on every object alike, so neither the custom-accessor fallback nor a Map-backed model's catch-all applies; rejecting"); + return false; + } if (takesParameter(model, rootProperty, paramDepth)) { LOG.debug("Property [{}] is taken by the ModelDriven model, exempting from @StrutsParameter annotation requirement", rootProperty); return true; } - if (CLASS_PROPERTY.equals(rootProperty)) { - LOG.debug("Property [class] is not an unknown property but Object.getClass() on every object alike, so the fallback for a custom accessor does not apply; rejecting"); - return false; - } if (takesParameter(action, rootProperty, paramDepth)) { LOG.debug("Property [{}] is taken by the ModelDriven action itself, applying the @StrutsParameter annotation requirement", rootProperty); diff --git a/core/src/test/java/org/apache/struts2/interceptor/parameter/ParameterAuthorizerTest.java b/core/src/test/java/org/apache/struts2/interceptor/parameter/ParameterAuthorizerTest.java index 3313f7001..c88393165 100644 --- a/core/src/test/java/org/apache/struts2/interceptor/parameter/ParameterAuthorizerTest.java +++ b/core/src/test/java/org/apache/struts2/interceptor/parameter/ParameterAuthorizerTest.java @@ -491,6 +491,20 @@ public class ParameterAuthorizerTest { assertThat(authorizer.isAuthorized("localizedTextProvider", action.getModel(), action)).isFalse(); } + @Test + public void modelDriven_mapModel_classAtDepthZero_rejected() { + // A Map-backed model takes any depth-0 name through map.put, "class" included, so without checking + // "class" before the model it would be exempted where the non-ModelDriven path rejects it. "class" is + // Object.getClass() on everything, never request surface, so it is rejected whatever the model is. + var action = new MapModelAction(); + var valueStack = new StubValueStack(); + valueStack.push(action); + valueStack.push(action.getModel()); + ActionContext.of().withValueStack(valueStack).bind(); + + assertThat(authorizer.isAuthorized("class", action.getModel(), action)).isFalse(); + } + // --- Inner test classes --- public static class PreviousAction {
