This is an automated email from the ASF dual-hosted git repository. kusal pushed a commit to branch fix/WW-5468-modeldriven-2 in repository https://gitbox.apache.org/repos/asf/struts.git
commit a57f51cc2bdd3d3501327c1c9b0798e314918152 Author: Kusal Kithul-Godage <g...@kusal.io> AuthorDate: Fri Oct 11 20:55:50 2024 +1100 WW-5468 Reworked fix, exempt ModelDriven Action parameters --- .../java/com/opensymphony/xwork2/ModelDriven.java | 5 +- .../parameter/ParametersInterceptor.java | 88 ++++++++++------------ .../parameter/StrutsParameterAnnotationTest.java | 25 +++--- 3 files changed, 58 insertions(+), 60 deletions(-) diff --git a/core/src/main/java/com/opensymphony/xwork2/ModelDriven.java b/core/src/main/java/com/opensymphony/xwork2/ModelDriven.java index 59641a997..fc7f9a348 100644 --- a/core/src/main/java/com/opensymphony/xwork2/ModelDriven.java +++ b/core/src/main/java/com/opensymphony/xwork2/ModelDriven.java @@ -30,10 +30,13 @@ public interface ModelDriven<T> { /** * Gets the model to be pushed onto the ValueStack instead of the Action itself. + * <p> + * Please be aware that all setters and getters of every depth on the object returned by this method are available + * for user parameter injection! * * @return the model */ - @StrutsParameter + @StrutsParameter(depth = Integer.MAX_VALUE) T getModel(); } 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 5d1010a0b..67e0d9142 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 @@ -259,19 +259,19 @@ public class ParametersInterceptor extends MethodFilterInterceptor { protected ValueStack toNewStack(ValueStack stack) { ValueStack newStack = valueStackFactory.createValueStack(stack); - if (newStack instanceof ClearableValueStack) { - ((ClearableValueStack) newStack).clearContextValues(); + if (newStack instanceof ClearableValueStack clearable) { + clearable.clearContextValues(); newStack.getActionContext().withLocale(stack.getActionContext().getLocale()).withValueStack(stack); } return newStack; } protected void applyMemberAccessProperties(ValueStack stack) { - if (!(stack instanceof MemberAccessValueStack)) { + if (!(stack instanceof MemberAccessValueStack accessValueStack)) { return; } - ((MemberAccessValueStack) stack).useAcceptProperties(acceptedPatterns.getAcceptedPatterns()); - ((MemberAccessValueStack) stack).useExcludeProperties(excludedPatterns.getExcludedPatterns()); + accessValueStack.useAcceptProperties(acceptedPatterns.getAcceptedPatterns()); + accessValueStack.useExcludeProperties(excludedPatterns.getExcludedPatterns()); } protected Map<String, Parameter> toAcceptableParameters(HttpParameters parameters, Object action) { @@ -333,7 +333,7 @@ public class ParametersInterceptor extends MethodFilterInterceptor { } protected boolean isAcceptableParameterNameAware(String name, Object action) { - return !(action instanceof ParameterNameAware) || ((ParameterNameAware) action).acceptableParameterName(name); + return !(action instanceof ParameterNameAware nameAware) || nameAware.acceptableParameterName(name); } /** @@ -349,7 +349,15 @@ public class ParametersInterceptor extends MethodFilterInterceptor { } long paramDepth = name.codePoints().mapToObj(c -> (char) c).filter(NESTING_CHARS::contains).count(); - if (!isModelDriven(action) && requireAnnotationsTransitionMode && paramDepth == 0) { + + 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 com.opensymphony.xwork2.ModelDriven#getModel) + return hasValidAnnotatedMember("model", action, paramDepth + 1); + } + + if (requireAnnotationsTransitionMode && paramDepth == 0) { + LOG.debug("Annotation transition mode enabled, exempting non-nested parameter [{}] from @StrutsParameter annotation requirement", name); return true; } @@ -366,39 +374,34 @@ public class ParametersInterceptor extends MethodFilterInterceptor { * save computation by checking this last. */ protected boolean hasValidAnnotatedMember(String rootProperty, Object action, long paramDepth) { - final String property = isModelDriven(action) ? "model" : rootProperty; - LOG.debug("Using [{}] as a root property of [{}]", property, action.getClass().getSimpleName()); - + LOG.debug("Checking Action [{}] for a matching, correctly annotated member for property [{}]", + action.getClass().getSimpleName(), rootProperty); BeanInfo beanInfo = getBeanInfo(action); if (beanInfo == null) { - return hasValidAnnotatedField(action, property, paramDepth); + return hasValidAnnotatedField(action, rootProperty, paramDepth); } Optional<PropertyDescriptor> propDescOpt = Arrays.stream(beanInfo.getPropertyDescriptors()) - .filter(desc -> desc.getName().equals(property)).findFirst(); + .filter(desc -> desc.getName().equals(rootProperty)).findFirst(); if (propDescOpt.isEmpty()) { - return hasValidAnnotatedField(action, property, paramDepth); + return hasValidAnnotatedField(action, rootProperty, paramDepth); } if (hasValidAnnotatedPropertyDescriptor(action, propDescOpt.get(), paramDepth)) { return true; } - return hasValidAnnotatedField(action, property, paramDepth); + return hasValidAnnotatedField(action, rootProperty, paramDepth); } protected boolean hasValidAnnotatedPropertyDescriptor(Object action, PropertyDescriptor propDesc, long paramDepth) { - Method relevantMethod = getRelevantMethod(action, propDesc, paramDepth); - + Method relevantMethod = paramDepth == 0 ? propDesc.getWriteMethod() : propDesc.getReadMethod(); if (relevantMethod == null) { - LOG.debug("No such property [{}] in action [{}]", propDesc.getName(), action.getClass().getSimpleName()); return false; } - - LOG.debug("Using [{}] as a property of [{}]", relevantMethod.getName(), action.getClass().getSimpleName()); if (getPermittedInjectionDepth(relevantMethod) < paramDepth) { String logMessage = format( - "Parameter injection for method [%s] on action [%s] rejected. Ensure it is annotated with @StrutsParameter with an appropriate 'depth'.", + "Parameter injection for method [%s] on Action [%s] rejected. Ensure it is annotated with @StrutsParameter with an appropriate 'depth'.", relevantMethod.getName(), relevantMethod.getDeclaringClass().getName()); if (devMode) { @@ -408,11 +411,10 @@ public class ParametersInterceptor extends MethodFilterInterceptor { } return false; } - if (isModelDriven(action)) { - allowlistClass(propDesc.getPropertyType()); - } + LOG.debug("Success: Matching annotated method [{}] found for property [{}] of depth [{}] on Action [{}]", + relevantMethod.getName(), propDesc.getName(), paramDepth, action.getClass().getSimpleName()); if (paramDepth >= 1) { - allowlistClass(relevantMethod.getReturnType()); + allowlistClass(propDesc.getPropertyType()); } if (paramDepth >= 2) { allowlistReturnTypeIfParameterized(relevantMethod); @@ -420,16 +422,6 @@ public class ParametersInterceptor extends MethodFilterInterceptor { return true; } - private Method getRelevantMethod(Object action, PropertyDescriptor propDesc, long paramDepth) { - Method useReadOrWriteMethod = isModelDriven(action) ? propDesc.getReadMethod() : propDesc.getWriteMethod(); - return paramDepth == 0 ? useReadOrWriteMethod : propDesc.getReadMethod(); - } - - private boolean isModelDriven(Object action) { - return action instanceof ModelDriven; - } - - protected void allowlistReturnTypeIfParameterized(Method method) { allowlistParameterizedTypeArg(method.getGenericReturnType()); } @@ -459,22 +451,22 @@ public class ParametersInterceptor extends MethodFilterInterceptor { } protected boolean hasValidAnnotatedField(Object action, String fieldName, long paramDepth) { + LOG.debug("No matching annotated method found for property [{}] of depth [{}] on Action [{}], now also checking for public field", + fieldName, paramDepth, action.getClass().getSimpleName()); Field field; - LOG.debug("Checking if field [{}] of [{}] is annotated with [{}]", - fieldName, action.getClass().getSimpleName(), StrutsParameter.class.getSimpleName()); try { field = action.getClass().getDeclaredField(fieldName); } catch (NoSuchFieldException e) { - LOG.debug("Field [{}] not found", fieldName); + LOG.debug("Matching field for property [{}] not found on Action [{}]", fieldName, action.getClass().getSimpleName()); return false; } if (!Modifier.isPublic(field.getModifiers())) { - LOG.debug("Field [{}] is not public", fieldName); + LOG.debug("Matching field [{}] is not public on Action [{}]", field.getName(), action.getClass().getSimpleName()); return false; } if (getPermittedInjectionDepth(field) < paramDepth) { String logMessage = format( - "Parameter injection for field [%s] on action [%s] rejected. Ensure it is annotated with @StrutsParameter with an appropriate 'depth'.", + "Parameter injection for field [%s] on Action [%s] rejected. Ensure it is annotated with @StrutsParameter with an appropriate 'depth'.", field.getName(), action.getClass().getName()); if (devMode) { @@ -484,6 +476,8 @@ public class ParametersInterceptor extends MethodFilterInterceptor { } return false; } + LOG.debug("Success: Matching annotated public field [{}] found for property of depth [{}] on Action [{}]", + field.getName(), paramDepth, action.getClass().getSimpleName()); if (paramDepth >= 1) { allowlistClass(field.getType()); } @@ -537,7 +531,7 @@ public class ParametersInterceptor extends MethodFilterInterceptor { } protected boolean isAcceptableParameterValueAware(Parameter param, Object action) { - return !(action instanceof ParameterValueAware) || ((ParameterValueAware) action).acceptableParameterValue(param.getValue()); + return !(action instanceof ParameterValueAware valueAware) || valueAware.acceptableParameterValue(param.getValue()); } /** @@ -616,9 +610,9 @@ public class ParametersInterceptor extends MethodFilterInterceptor { if (!matchLength) { if (devMode) { LOG.warn("Parameter [{}] is too long, allowed length is [{}]. Use Interceptor Parameter Overriding " + - "to override the limit, see more at\n" + - "https://struts.apache.org/core-developers/interceptors.html#interceptor-parameter-overriding", - name, paramNameMaxLength); + "to override the limit, see more at\n" + + "https://struts.apache.org/core-developers/interceptors.html#interceptor-parameter-overriding", + name, paramNameMaxLength); } else { LOG.warn("Parameter [{}] is too long, allowed length is [{}]", name, paramNameMaxLength); } @@ -631,7 +625,7 @@ public class ParametersInterceptor extends MethodFilterInterceptor { if (!result.isAccepted()) { if (devMode) { LOG.warn("Parameter [{}] didn't match accepted pattern [{}]! See Accepted / Excluded patterns at\n" + - "https://struts.apache.org/security/#accepted--excluded-patterns", + "https://struts.apache.org/security/#accepted--excluded-patterns", paramName, result.getAcceptedPattern()); } else { LOG.debug("Parameter [{}] didn't match accepted pattern [{}]!", paramName, result.getAcceptedPattern()); @@ -646,7 +640,7 @@ public class ParametersInterceptor extends MethodFilterInterceptor { if (result.isExcluded()) { if (devMode) { LOG.warn("Parameter [{}] matches excluded pattern [{}]! See Accepted / Excluded patterns at\n" + - "https://struts.apache.org/security/#accepted--excluded-patterns", + "https://struts.apache.org/security/#accepted--excluded-patterns", paramName, result.getExcludedPattern()); } else { LOG.debug("Parameter [{}] matches excluded pattern [{}]!", paramName, result.getExcludedPattern()); @@ -665,7 +659,7 @@ public class ParametersInterceptor extends MethodFilterInterceptor { if (excludedValuePattern.matcher(value).matches()) { if (devMode) { LOG.warn("Parameter value [{}] matches excluded pattern [{}]! See Accepting/Excluding parameter values at\n" + - "https://struts.apache.org/core-developers/parameters-interceptor#excluding-parameter-values", + "https://struts.apache.org/core-developers/parameters-interceptor#excluding-parameter-values", value, excludedValuePatterns); } else { LOG.debug("Parameter value [{}] matches excluded pattern [{}]", value, excludedValuePattern); @@ -688,7 +682,7 @@ public class ParametersInterceptor extends MethodFilterInterceptor { } if (devMode) { LOG.warn("Parameter value [{}] didn't match accepted pattern [{}]! See Accepting/Excluding parameter values at\n" + - "https://struts.apache.org/core-developers/parameters-interceptor#excluding-parameter-values", + "https://struts.apache.org/core-developers/parameters-interceptor#excluding-parameter-values", value, acceptedValuePatterns); } else { LOG.debug("Parameter value [{}] was not accepted!", value); 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 eb44fc6fe..8116952b6 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 @@ -18,7 +18,9 @@ */ package org.apache.struts2.interceptor.parameter; +import com.opensymphony.xwork2.ActionContext; import com.opensymphony.xwork2.ModelDriven; +import com.opensymphony.xwork2.StubValueStack; import com.opensymphony.xwork2.security.AcceptedPatternsChecker; import com.opensymphony.xwork2.security.NotExcludedAcceptedPatternsChecker; import org.apache.commons.lang3.ClassUtils; @@ -64,6 +66,7 @@ public class StrutsParameterAnnotationTest { @After public void tearDown() throws Exception { threadAllowlist.clearAllowlist(); + ActionContext.clear(); } private void testParameter(Object action, String paramName, boolean shouldContain) { @@ -261,8 +264,15 @@ public class StrutsParameterAnnotationTest { @Test public void publicModelPojo() { - parametersInterceptor.setRequireAnnotationsTransitionMode(Boolean.TRUE.toString()); - testParameter(new ModelAction(), "name", true); + var action = new ModelAction(); + + // Emulate ModelDrivenInterceptor running previously + var valueStack = new StubValueStack(); + valueStack.push(action.getModel()); + ActionContext.of().withValueStack(valueStack).bind(); + + testParameter(action, "name", true); + testParameter(action, "name.nested", true); assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Object.class, Pojo.class)); } @@ -352,21 +362,12 @@ public class StrutsParameterAnnotationTest { static class ModelAction implements ModelDriven<Pojo> { - @StrutsParameter + @Override public Pojo getModel() { return new Pojo(); } } static class Pojo { - private String name; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } } }