Skip to content

Commit

Permalink
Merge pull request #1072 from apache/fix/WW-5468-modeldriven-2
Browse files Browse the repository at this point in the history
WW-5468 Exempt ModelDriven Actions from @StrutsParameter requirement
  • Loading branch information
kusalk committed Nov 1, 2024
1 parent 8566c14 commit 7cdcd84
Show file tree
Hide file tree
Showing 17 changed files with 281 additions and 248 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public String execute() throws Exception {
}

@Override
public Object getModel() {
public Gangster getModel() {
return new Gangster();
}
}
6 changes: 6 additions & 0 deletions core/src/main/java/org/apache/struts2/ModelDriven.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
*/
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.
Expand All @@ -28,9 +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(depth = Integer.MAX_VALUE)
T getModel();

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import org.apache.commons.lang3.ClassUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.ModelDriven;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.action.NoParameters;
import org.apache.struts2.action.ParameterNameAware;
Expand Down Expand Up @@ -348,7 +349,15 @@ protected boolean isParameterAnnotatedAndAllowlist(String name, Object action) {
}

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 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;
}

Expand All @@ -365,6 +374,8 @@ protected boolean isParameterAnnotatedAndAllowlist(String name, Object action) {
* save computation by checking this last.
*/
protected boolean hasValidAnnotatedMember(String rootProperty, Object action, long paramDepth) {
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, rootProperty, paramDepth);
Expand Down Expand Up @@ -399,7 +410,7 @@ protected boolean hasValidAnnotatedPropertyDescriptor(Object action, PropertyDes
}
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) {
Expand All @@ -409,8 +420,10 @@ protected boolean hasValidAnnotatedPropertyDescriptor(Object action, PropertyDes
}
return false;
}
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);
Expand Down Expand Up @@ -447,19 +460,23 @@ protected void allowlistClass(Class<?> clazz) {
}

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;
try {
field = action.getClass().getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
LOG.debug("Matching field for property [{}] not found on Action [{}]", fieldName, action.getClass().getSimpleName());
return false;
}
if (!Modifier.isPublic(field.getModifiers())) {
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'.",
fieldName,
"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) {
notifyDeveloperOfError(LOG, action, logMessage);
Expand All @@ -468,6 +485,8 @@ protected boolean hasValidAnnotatedField(Object action, String fieldName, long p
}
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());
}
Expand Down Expand Up @@ -629,7 +648,7 @@ protected boolean isAccepted(String paramName) {
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());
Expand All @@ -644,8 +663,8 @@ protected boolean isExcluded(String paramName) {
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",
paramName, result.getExcludedPattern());
"https://struts.apache.org/security/#accepted--excluded-patterns",
paramName, result.getExcludedPattern());
} else {
LOG.debug("Parameter [{}] matches excluded pattern [{}]!", paramName, result.getExcludedPattern());
}
Expand All @@ -663,8 +682,8 @@ protected boolean isParamValueExcluded(String value) {
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",
value, excludedValuePatterns);
"https://struts.apache.org/core-developers/parameters-interceptor#excluding-parameter-values",
value, excludedValuePatterns);
} else {
LOG.debug("Parameter value [{}] matches excluded pattern [{}]", value, excludedValuePattern);
}
Expand All @@ -686,8 +705,8 @@ protected boolean isParamValueAccepted(String value) {
}
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",
value, acceptedValuePatterns);
"https://struts.apache.org/core-developers/parameters-interceptor#excluding-parameter-values",
value, acceptedValuePatterns);
} else {
LOG.debug("Parameter value [{}] was not accepted!", value);
}
Expand Down Expand Up @@ -757,7 +776,7 @@ public void setAcceptedValuePatterns(String commaDelimitedPatterns) {
LOG.debug("Sets accepted value patterns to [{}], note this may impact the safety of your application!", patterns);
} else {
LOG.warn("Replacing accepted patterns [{}] with [{}], be aware that this may impact safety of your application!",
acceptedValuePatterns, patterns);
acceptedValuePatterns, patterns);
}
acceptedValuePatterns = new HashSet<>(patterns.size());
try {
Expand All @@ -782,7 +801,7 @@ public void setExcludedValuePatterns(String commaDelimitedPatterns) {
LOG.debug("Setting excluded value patterns to [{}]", patterns);
} else {
LOG.warn("Replacing excluded value patterns [{}] with [{}], be aware that this may impact safety of your application!",
excludedValuePatterns, patterns);
excludedValuePatterns, patterns);
}
excludedValuePatterns = new HashSet<>(patterns.size());
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,8 @@ public String getFoo() {
/**
* @return the model to be pushed onto the ValueStack after the Action itself
*/
@StrutsParameter(depth = 2)
@Override
public Object getModel() {
public TestBean getModel() {
return model;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,8 @@ public String getFoo() {
/**
* @return the model to be pushed onto the ValueStack after the Action itself
*/
@StrutsParameter(depth = 2)
@Override
public Object getModel() {
public AnnotatedTestBean getModel() {
return model;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ protected void tearDown() throws Exception {
}


public class ModelDrivenAction extends ActionSupport implements ModelDriven {
public class ModelDrivenAction extends ActionSupport implements ModelDriven<Object> {

@Override
public Object getModel() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
package com.opensymphony.xwork2.test;

import com.opensymphony.xwork2.ModelDrivenAction;
import org.apache.struts2.interceptor.parameter.StrutsParameter;


/**
Expand All @@ -35,9 +34,8 @@ public class ModelDrivenAction2 extends ModelDrivenAction {
/**
* @return the model to be pushed onto the ValueStack after the Action itself
*/
@StrutsParameter(depth = 3)
@Override
public Object getModel() {
public TestBean2 getModel() {
return model;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
package com.opensymphony.xwork2.test;

import com.opensymphony.xwork2.ModelDrivenAnnotationAction;
import org.apache.struts2.interceptor.parameter.StrutsParameter;


/**
Expand All @@ -36,9 +35,8 @@ public class ModelDrivenAnnotationAction2 extends ModelDrivenAnnotationAction {
/**
* @return the model to be pushed onto the ValueStack after the Action itself
*/
@StrutsParameter(depth = 3)
@Override
public Object getModel() {
public AnnotationTestBean2 getModel() {
return model;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package com.opensymphony.xwork2.test.subtest;

import com.opensymphony.xwork2.ModelDrivenAction;
import com.opensymphony.xwork2.TestBean;

/**
* Extends ModelDrivenAction to return a null model.
Expand All @@ -31,7 +32,7 @@ public class NullModelDrivenAction extends ModelDrivenAction {
* @return the model to be pushed onto the ValueStack instead of the Action itself
*/
@Override
public Object getModel() {
public TestBean getModel() {
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
package com.opensymphony.xwork2.validator;

import com.opensymphony.xwork2.ModelDriven;
import org.apache.struts2.interceptor.parameter.StrutsParameter;
import com.opensymphony.xwork2.TestBean;


/**
Expand All @@ -33,9 +33,8 @@ public class VisitorValidatorModelAction extends VisitorValidatorTestAction impl
/**
* @return the model to be pushed onto the ValueStack instead of the Action itself
*/
@StrutsParameter(depth = 2)
@Override
public Object getModel() {
public TestBean getModel() {
return getBean();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@
*/
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 com.opensymphony.xwork2.util.ValueStack;
import org.apache.commons.lang3.ClassUtils;
import org.apache.struts2.dispatcher.HttpParameters;
import org.apache.struts2.dispatcher.Parameter;
Expand Down Expand Up @@ -63,6 +67,7 @@ public void setUp() throws Exception {
@After
public void tearDown() throws Exception {
threadAllowlist.clearAllowlist();
ActionContext.clear();
}

private void testParameter(Object action, String paramName, boolean shouldContain) {
Expand All @@ -80,7 +85,7 @@ private void testParameter(Object action, String paramName, boolean shouldContai
}
}

private Set<Class<?>> getParentClasses(Class<?> ...clazzes) {
private Set<Class<?>> getParentClasses(Class<?>... clazzes) {
Set<Class<?>> set = new HashSet<>();
for (Class<?> clazz : clazzes) {
set.add(clazz);
Expand Down Expand Up @@ -258,8 +263,21 @@ public void publicStrNotAnnotatedMethod_transitionMode() {
testParameter(new MethodAction(), "publicStrNotAnnotated", true);
}

@Test
public void publicModelPojo() {
ModelAction action = new ModelAction();

// Emulate ModelDrivenInterceptor running previously
ValueStack 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));
}

class FieldAction {
static class FieldAction {
@StrutsParameter
private String privateStr;

Expand All @@ -275,7 +293,7 @@ class FieldAction {
public Pojo publicPojoDepthZero;

@StrutsParameter(depth = 1)
public Pojo publicPojoDepthOne ;
public Pojo publicPojoDepthOne;

@StrutsParameter(depth = 2)
public Pojo publicPojoDepthTwo;
Expand All @@ -290,7 +308,7 @@ class FieldAction {
public Map<String, Pojo> publicPojoMapDepthTwo;
}

class MethodAction {
static class MethodAction {

@StrutsParameter
private void setPrivateStr(String str) {
Expand Down Expand Up @@ -343,6 +361,14 @@ public Map<String, Pojo> getPublicPojoMapDepthTwo() {
}
}

class Pojo {
static class ModelAction implements ModelDriven<Pojo> {

@Override
public Pojo getModel() {
return new Pojo();
}
}

static class Pojo {
}
}
Loading

0 comments on commit 7cdcd84

Please sign in to comment.