This is an automated email from the ASF dual-hosted git repository. kusal pushed a commit to branch WW-5534-model-driven-struts-param in repository https://gitbox.apache.org/repos/asf/struts.git
commit 59e6baade0ec6ded5e0f42d7633b041578df20cf Author: Kusal Kithul-Godage <g...@kusal.io> AuthorDate: Thu Mar 6 23:50:44 2025 +1100 WW-5534 Proper fix ModelDriven parameter injection and allowlisting --- .../apache/struts2/showcase/ModelDrivenTest.java | 63 ++++++++++++++++++++++ .../main/java/org/apache/struts2/ModelDriven.java | 3 -- .../java/org/apache/struts2/components/Debug.java | 13 ++--- .../struts2/components/IteratorComponent.java | 6 +-- .../interceptor/ExceptionMappingInterceptor.java | 6 +-- .../interceptor/ModelDrivenInterceptor.java | 15 ++++-- .../debugging/DebuggingInterceptor.java | 7 +-- .../parameter/ParametersInterceptor.java | 19 +++---- .../org/apache/struts2/ognl/ThreadAllowlist.java | 11 ++++ .../parameter/StrutsParameterAnnotationTest.java | 10 ++-- 10 files changed, 111 insertions(+), 42 deletions(-) diff --git a/apps/showcase/src/test/java/it/org/apache/struts2/showcase/ModelDrivenTest.java b/apps/showcase/src/test/java/it/org/apache/struts2/showcase/ModelDrivenTest.java new file mode 100644 index 000000000..b3656fc7c --- /dev/null +++ b/apps/showcase/src/test/java/it/org/apache/struts2/showcase/ModelDrivenTest.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package it.org.apache.struts2.showcase; + +import org.htmlunit.WebClient; +import org.htmlunit.html.HtmlForm; +import org.htmlunit.html.HtmlPage; +import org.htmlunit.html.HtmlSubmitInput; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ModelDrivenTest { + + private WebClient webClient; + + @Before + public void setUp() throws Exception { + webClient = new WebClient(); + } + + @After + public void tearDown() throws Exception { + webClient.close(); + } + + @Test + public void submit() throws Exception { + HtmlPage page = webClient.getPage(ParameterUtils.getBaseUrl() + "/modelDriven/modelDriven.action"); + HtmlForm form = page.getForms().get(0); + + form.getInputByName("name").setValue("Johannes"); + form.getInputByName("age").setValue("21"); + form.getInputByName("bustedBefore").setChecked(true); + form.getTextAreaByName("description").setText("Deals bugs"); + + HtmlSubmitInput button = form.getInputByValue("Submit"); + page = button.click(); + + assertThat(page.getElementById("name").asNormalizedText()).isEqualTo("Johannes"); + assertThat(page.getElementById("age").asNormalizedText()).isEqualTo("21"); + assertThat(page.getElementById("bustedBefore").asNormalizedText()).isEqualTo("true"); + assertThat(page.getElementById("description").asNormalizedText()).isEqualTo("Deals bugs"); + } +} diff --git a/core/src/main/java/org/apache/struts2/ModelDriven.java b/core/src/main/java/org/apache/struts2/ModelDriven.java index 993a6f5eb..dd21eb2f7 100644 --- a/core/src/main/java/org/apache/struts2/ModelDriven.java +++ b/core/src/main/java/org/apache/struts2/ModelDriven.java @@ -18,8 +18,6 @@ */ package org.apache.struts2; -import org.apache.struts2.interceptor.parameter.StrutsParameter; - /** * ModelDriven Actions provide a model object to be pushed onto the ValueStack * in addition to the Action itself, allowing a FormBean type approach like Struts. @@ -36,7 +34,6 @@ public interface ModelDriven<T> { * * @return the model */ - @StrutsParameter(depth = Integer.MAX_VALUE) T getModel(); } diff --git a/core/src/main/java/org/apache/struts2/components/Debug.java b/core/src/main/java/org/apache/struts2/components/Debug.java index 702f9e32b..65c210fa0 100644 --- a/core/src/main/java/org/apache/struts2/components/Debug.java +++ b/core/src/main/java/org/apache/struts2/components/Debug.java @@ -18,18 +18,17 @@ */ package org.apache.struts2.components; -import org.apache.commons.lang3.ClassUtils; +import org.apache.struts2.StrutsException; +import org.apache.struts2.dispatcher.PrepareOperations; import org.apache.struts2.inject.Inject; import org.apache.struts2.ognl.ThreadAllowlist; import org.apache.struts2.util.CompoundRoot; import org.apache.struts2.util.ValueStack; import org.apache.struts2.util.reflection.ReflectionProvider; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.apache.struts2.StrutsException; -import org.apache.struts2.dispatcher.PrepareOperations; import org.apache.struts2.views.annotations.StrutsTag; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.io.Writer; import java.util.ArrayList; import java.util.Iterator; @@ -94,9 +93,7 @@ public class Debug extends UIBean { } private void allowListClass(Object o) { - threadAllowlist.allowClass(o.getClass()); - ClassUtils.getAllSuperclasses(o.getClass()).forEach(threadAllowlist::allowClass); - ClassUtils.getAllInterfaces(o.getClass()).forEach(threadAllowlist::allowClass); + threadAllowlist.allowClassHierarchy(o.getClass()); } @Override diff --git a/core/src/main/java/org/apache/struts2/components/IteratorComponent.java b/core/src/main/java/org/apache/struts2/components/IteratorComponent.java index f45452ddf..c31f02486 100644 --- a/core/src/main/java/org/apache/struts2/components/IteratorComponent.java +++ b/core/src/main/java/org/apache/struts2/components/IteratorComponent.java @@ -18,12 +18,12 @@ */ package org.apache.struts2.components; -import org.apache.struts2.inject.Inject; -import org.apache.struts2.util.ValueStack; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.struts2.inject.Inject; import org.apache.struts2.ognl.ThreadAllowlist; import org.apache.struts2.util.MakeIterator; +import org.apache.struts2.util.ValueStack; import org.apache.struts2.views.annotations.StrutsTag; import org.apache.struts2.views.annotations.StrutsTagAttribute; import org.apache.struts2.views.jsp.IteratorStatus; @@ -307,7 +307,7 @@ public class IteratorComponent extends ContextBean { stack.push(currentValue); if (currentValue != null) { - threadAllowlist.allowClass(currentValue.getClass()); + threadAllowlist.allowClassHierarchy(currentValue.getClass()); } String var = getVar(); diff --git a/core/src/main/java/org/apache/struts2/interceptor/ExceptionMappingInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/ExceptionMappingInterceptor.java index b64bd81e1..acc906500 100644 --- a/core/src/main/java/org/apache/struts2/interceptor/ExceptionMappingInterceptor.java +++ b/core/src/main/java/org/apache/struts2/interceptor/ExceptionMappingInterceptor.java @@ -215,9 +215,9 @@ public class ExceptionMappingInterceptor extends AbstractInterceptor { HttpParameters parameters = HttpParameters.create(mappingParams).build(); invocation.getInvocationContext().withParameters(parameters); result = mappingConfig.getResult(); - ExceptionHolder holder = new ExceptionHolder(e); - threadAllowlist.allowClass(holder.getClass()); - threadAllowlist.allowClass(e.getClass()); + var holder = new ExceptionHolder(e); + threadAllowlist.allowClassHierarchy(ExceptionHolder.class); + threadAllowlist.allowClassHierarchy(e.getClass()); publishException(invocation, holder); } else { throw e; diff --git a/core/src/main/java/org/apache/struts2/interceptor/ModelDrivenInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/ModelDrivenInterceptor.java index fb8cb2dd5..5bdb295ee 100644 --- a/core/src/main/java/org/apache/struts2/interceptor/ModelDrivenInterceptor.java +++ b/core/src/main/java/org/apache/struts2/interceptor/ModelDrivenInterceptor.java @@ -20,6 +20,8 @@ package org.apache.struts2.interceptor; import org.apache.struts2.ActionInvocation; import org.apache.struts2.ModelDriven; +import org.apache.struts2.inject.Inject; +import org.apache.struts2.ognl.ThreadAllowlist; import org.apache.struts2.util.CompoundRoot; import org.apache.struts2.util.ValueStack; @@ -79,20 +81,27 @@ import org.apache.struts2.util.ValueStack; public class ModelDrivenInterceptor extends AbstractInterceptor { protected boolean refreshModelBeforeResult = false; + private ThreadAllowlist threadAllowlist; public void setRefreshModelBeforeResult(boolean val) { this.refreshModelBeforeResult = val; } + @Inject + public void setThreadAllowlist(ThreadAllowlist threadAllowlist) { + this.threadAllowlist = threadAllowlist; + } + @Override public String intercept(ActionInvocation invocation) throws Exception { Object action = invocation.getAction(); - if (action instanceof ModelDriven modelDriven) { + if (action instanceof ModelDriven<?> modelDriven) { ValueStack stack = invocation.getStack(); Object model = modelDriven.getModel(); - if (model != null) { - stack.push(model); + if (model != null) { + stack.push(model); + threadAllowlist.allowClassHierarchy(model.getClass()); } if (refreshModelBeforeResult) { invocation.addPreResultListener(new RefreshModelBeforeResult(modelDriven, model)); diff --git a/core/src/main/java/org/apache/struts2/interceptor/debugging/DebuggingInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/debugging/DebuggingInterceptor.java index 1fa9f7b18..ed8b2c922 100644 --- a/core/src/main/java/org/apache/struts2/interceptor/debugging/DebuggingInterceptor.java +++ b/core/src/main/java/org/apache/struts2/interceptor/debugging/DebuggingInterceptor.java @@ -18,8 +18,6 @@ */ package org.apache.struts2.interceptor.debugging; -import jakarta.servlet.http.HttpServletResponse; -import org.apache.commons.lang3.ClassUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.struts2.ActionContext; @@ -38,6 +36,7 @@ import org.apache.struts2.util.reflection.ReflectionProvider; import org.apache.struts2.views.freemarker.FreemarkerManager; import org.apache.struts2.views.freemarker.FreemarkerResult; +import jakarta.servlet.http.HttpServletResponse; import java.beans.BeanInfo; import java.beans.Introspector; import java.beans.PropertyDescriptor; @@ -273,9 +272,7 @@ public class DebuggingInterceptor extends AbstractInterceptor { private void allowListClass(Object o) { if (o != null) { - threadAllowlist.allowClass(o.getClass()); - ClassUtils.getAllSuperclasses(o.getClass()).forEach(threadAllowlist::allowClass); - ClassUtils.getAllInterfaces(o.getClass()).forEach(threadAllowlist::allowClass); + threadAllowlist.allowClassHierarchy(o.getClass()); } } diff --git a/core/src/main/java/org/apache/struts2/interceptor/parameter/ParametersInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/parameter/ParametersInterceptor.java index 4cd1c3faa..32cffc291 100644 --- a/core/src/main/java/org/apache/struts2/interceptor/parameter/ParametersInterceptor.java +++ b/core/src/main/java/org/apache/struts2/interceptor/parameter/ParametersInterceptor.java @@ -19,7 +19,6 @@ package org.apache.struts2.interceptor.parameter; import org.apache.commons.lang3.BooleanUtils; -import org.apache.commons.lang3.ClassUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.struts2.ActionContext; @@ -358,9 +357,8 @@ public class ParametersInterceptor extends MethodFilterInterceptor { long paramDepth = name.codePoints().mapToObj(c -> (char) c).filter(NESTING_CHARS::contains).count(); if (action instanceof ModelDriven<?> && !ActionContext.getContext().getValueStack().peek().equals(action)) { - LOG.debug("Model driven Action detected, exempting from @StrutsParameter annotation requirement and OGNL allowlisting model type"); - // (Exempted by annotation on org.apache.struts2.ModelDriven#getModel) - return hasValidAnnotatedMember("model", action, paramDepth + 1); + LOG.debug("Model driven Action detected, exempting from @StrutsParameter annotation requirement"); + return true; } if (requireAnnotationsTransitionMode && paramDepth == 0) { @@ -447,15 +445,13 @@ public class ParametersInterceptor extends MethodFilterInterceptor { } protected void allowlistParamType(Type paramType) { - if (paramType instanceof Class) { - allowlistClass((Class<?>) paramType); + if (paramType instanceof Class<?> clazz) { + allowlistClass(clazz); } } protected void allowlistClass(Class<?> clazz) { - threadAllowlist.allowClass(clazz); - ClassUtils.getAllSuperclasses(clazz).forEach(threadAllowlist::allowClass); - ClassUtils.getAllInterfaces(clazz).forEach(threadAllowlist::allowClass); + threadAllowlist.allowClassHierarchy(clazz); } protected boolean hasValidAnnotatedField(Object action, String fieldName, long paramDepth) { @@ -527,10 +523,11 @@ public class ParametersInterceptor extends MethodFilterInterceptor { } protected BeanInfo getBeanInfo(Object action) { + Class<?> actionClass = ultimateClass(action); try { - return ognlUtil.getBeanInfo(ultimateClass(action)); + return ognlUtil.getBeanInfo(actionClass); } catch (IntrospectionException e) { - LOG.warn("Error introspecting Action {} for parameter injection validation", action.getClass(), e); + LOG.warn("Error introspecting Action {} for parameter injection validation", actionClass, e); return null; } } diff --git a/core/src/main/java/org/apache/struts2/ognl/ThreadAllowlist.java b/core/src/main/java/org/apache/struts2/ognl/ThreadAllowlist.java index e07cae64e..9fda5734b 100644 --- a/core/src/main/java/org/apache/struts2/ognl/ThreadAllowlist.java +++ b/core/src/main/java/org/apache/struts2/ognl/ThreadAllowlist.java @@ -18,6 +18,8 @@ */ package org.apache.struts2.ognl; +import org.apache.commons.lang3.ClassUtils; + import java.util.HashSet; import java.util.Set; @@ -34,6 +36,15 @@ public class ThreadAllowlist { private final ThreadLocal<Set<Class<?>>> allowlist = new ThreadLocal<>(); + /** + * @since 7.1.0 + */ + public void allowClassHierarchy(Class<?> clazz) { + allowClass(clazz); + ClassUtils.getAllSuperclasses(clazz).forEach(this::allowClass); + ClassUtils.getAllInterfaces(clazz).forEach(this::allowClass); + } + public void allowClass(Class<?> clazz) { if (allowlist.get() == null) { allowlist.set(new HashSet<>()); diff --git a/core/src/test/java/org/apache/struts2/interceptor/parameter/StrutsParameterAnnotationTest.java b/core/src/test/java/org/apache/struts2/interceptor/parameter/StrutsParameterAnnotationTest.java index 500574d34..54125fdef 100644 --- a/core/src/test/java/org/apache/struts2/interceptor/parameter/StrutsParameterAnnotationTest.java +++ b/core/src/test/java/org/apache/struts2/interceptor/parameter/StrutsParameterAnnotationTest.java @@ -381,7 +381,6 @@ public class StrutsParameterAnnotationTest { testParameter(action, "name", true); testParameter(action, "name.nested", true); - assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Object.class, Pojo.class)); } /** @@ -402,10 +401,9 @@ public class StrutsParameterAnnotationTest { testParameter(proxiedAction, "name", true); testParameter(proxiedAction, "name.nested", true); - assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Object.class, Pojo.class)); } - static class FieldAction { + public static class FieldAction { @StrutsParameter private String privateStr; @@ -436,7 +434,7 @@ public class StrutsParameterAnnotationTest { public Map<String, Pojo> publicPojoMapDepthTwo; } - static class MethodAction { + public static class MethodAction { @StrutsParameter private void setPrivateStr(String str) { @@ -489,7 +487,7 @@ public class StrutsParameterAnnotationTest { } } - static class ModelAction implements ModelDriven<Pojo> { + public static class ModelAction implements ModelDriven<Pojo> { @Override public Pojo getModel() { @@ -497,6 +495,6 @@ public class StrutsParameterAnnotationTest { } } - static class Pojo { + public static class Pojo { } }