Skip to content

Commit 96f838d

Browse files
committed
WW-5534 Proper fix ModelDriven parameter injection and allowlisting
1 parent 7ae52cc commit 96f838d

File tree

11 files changed

+119
-43
lines changed

11 files changed

+119
-43
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package it.org.apache.struts2.showcase;
20+
21+
import org.htmlunit.WebClient;
22+
import org.htmlunit.html.HtmlForm;
23+
import org.htmlunit.html.HtmlPage;
24+
import org.htmlunit.html.HtmlSubmitInput;
25+
import org.junit.After;
26+
import org.junit.Before;
27+
import org.junit.Test;
28+
29+
import static org.assertj.core.api.Assertions.assertThat;
30+
31+
public class ModelDrivenTest {
32+
33+
private WebClient webClient;
34+
35+
@Before
36+
public void setUp() throws Exception {
37+
webClient = new WebClient();
38+
}
39+
40+
@After
41+
public void tearDown() throws Exception {
42+
webClient.close();
43+
}
44+
45+
@Test
46+
public void submit() throws Exception {
47+
HtmlPage page = webClient.getPage(ParameterUtils.getBaseUrl() + "/modelDriven/modelDriven.action");
48+
HtmlForm form = page.getForms().get(0);
49+
50+
form.getInputByName("name").setValue("Johannes");
51+
form.getInputByName("age").setValue("21");
52+
form.getInputByName("bustedBefore").setChecked(true);
53+
form.getTextAreaByName("description").setText("Deals bugs");
54+
55+
HtmlSubmitInput button = form.getInputByValue("Submit");
56+
page = button.click();
57+
58+
assertThat(page.getElementById("name").asNormalizedText()).isEqualTo("Johannes");
59+
assertThat(page.getElementById("age").asNormalizedText()).isEqualTo("21");
60+
assertThat(page.getElementById("bustedBefore").asNormalizedText()).isEqualTo("true");
61+
assertThat(page.getElementById("description").asNormalizedText()).isEqualTo("Deals bugs");
62+
}
63+
}

core/src/main/java/org/apache/struts2/ModelDriven.java

-3
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818
*/
1919
package org.apache.struts2;
2020

21-
import org.apache.struts2.interceptor.parameter.StrutsParameter;
22-
2321
/**
2422
* ModelDriven Actions provide a model object to be pushed onto the ValueStack
2523
* in addition to the Action itself, allowing a FormBean type approach like Struts.
@@ -36,7 +34,6 @@ public interface ModelDriven<T> {
3634
*
3735
* @return the model
3836
*/
39-
@StrutsParameter(depth = Integer.MAX_VALUE)
4037
T getModel();
4138

4239
}

core/src/main/java/org/apache/struts2/components/Debug.java

+5-8
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,17 @@
1818
*/
1919
package org.apache.struts2.components;
2020

21-
import org.apache.commons.lang3.ClassUtils;
21+
import org.apache.struts2.StrutsException;
22+
import org.apache.struts2.dispatcher.PrepareOperations;
2223
import org.apache.struts2.inject.Inject;
2324
import org.apache.struts2.ognl.ThreadAllowlist;
2425
import org.apache.struts2.util.CompoundRoot;
2526
import org.apache.struts2.util.ValueStack;
2627
import org.apache.struts2.util.reflection.ReflectionProvider;
27-
import jakarta.servlet.http.HttpServletRequest;
28-
import jakarta.servlet.http.HttpServletResponse;
29-
import org.apache.struts2.StrutsException;
30-
import org.apache.struts2.dispatcher.PrepareOperations;
3128
import org.apache.struts2.views.annotations.StrutsTag;
3229

30+
import jakarta.servlet.http.HttpServletRequest;
31+
import jakarta.servlet.http.HttpServletResponse;
3332
import java.io.Writer;
3433
import java.util.ArrayList;
3534
import java.util.Iterator;
@@ -94,9 +93,7 @@ private void allowList(CompoundRoot root) {
9493
}
9594

9695
private void allowListClass(Object o) {
97-
threadAllowlist.allowClass(o.getClass());
98-
ClassUtils.getAllSuperclasses(o.getClass()).forEach(threadAllowlist::allowClass);
99-
ClassUtils.getAllInterfaces(o.getClass()).forEach(threadAllowlist::allowClass);
96+
threadAllowlist.allowClassHierarchy(o.getClass());
10097
}
10198

10299
@Override

core/src/main/java/org/apache/struts2/components/IteratorComponent.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@
1818
*/
1919
package org.apache.struts2.components;
2020

21-
import org.apache.struts2.inject.Inject;
22-
import org.apache.struts2.util.ValueStack;
2321
import org.apache.logging.log4j.LogManager;
2422
import org.apache.logging.log4j.Logger;
23+
import org.apache.struts2.inject.Inject;
2524
import org.apache.struts2.ognl.ThreadAllowlist;
2625
import org.apache.struts2.util.MakeIterator;
26+
import org.apache.struts2.util.ValueStack;
2727
import org.apache.struts2.views.annotations.StrutsTag;
2828
import org.apache.struts2.views.annotations.StrutsTagAttribute;
2929
import org.apache.struts2.views.jsp.IteratorStatus;
@@ -307,7 +307,7 @@ public boolean start(Writer writer) {
307307
stack.push(currentValue);
308308

309309
if (currentValue != null) {
310-
threadAllowlist.allowClass(currentValue.getClass());
310+
threadAllowlist.allowClassHierarchy(currentValue.getClass());
311311
}
312312

313313
String var = getVar();

core/src/main/java/org/apache/struts2/interceptor/ExceptionMappingInterceptor.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -215,9 +215,9 @@ public String intercept(ActionInvocation invocation) throws Exception {
215215
HttpParameters parameters = HttpParameters.create(mappingParams).build();
216216
invocation.getInvocationContext().withParameters(parameters);
217217
result = mappingConfig.getResult();
218-
ExceptionHolder holder = new ExceptionHolder(e);
219-
threadAllowlist.allowClass(holder.getClass());
220-
threadAllowlist.allowClass(e.getClass());
218+
var holder = new ExceptionHolder(e);
219+
threadAllowlist.allowClassHierarchy(ExceptionHolder.class);
220+
threadAllowlist.allowClassHierarchy(e.getClass());
221221
publishException(invocation, holder);
222222
} else {
223223
throw e;

core/src/main/java/org/apache/struts2/interceptor/ModelDrivenInterceptor.java

+12-3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
import org.apache.struts2.ActionInvocation;
2222
import org.apache.struts2.ModelDriven;
23+
import org.apache.struts2.inject.Inject;
24+
import org.apache.struts2.ognl.ThreadAllowlist;
2325
import org.apache.struts2.util.CompoundRoot;
2426
import org.apache.struts2.util.ValueStack;
2527

@@ -79,20 +81,27 @@
7981
public class ModelDrivenInterceptor extends AbstractInterceptor {
8082

8183
protected boolean refreshModelBeforeResult = false;
84+
private ThreadAllowlist threadAllowlist;
8285

8386
public void setRefreshModelBeforeResult(boolean val) {
8487
this.refreshModelBeforeResult = val;
8588
}
8689

90+
@Inject
91+
public void setThreadAllowlist(ThreadAllowlist threadAllowlist) {
92+
this.threadAllowlist = threadAllowlist;
93+
}
94+
8795
@Override
8896
public String intercept(ActionInvocation invocation) throws Exception {
8997
Object action = invocation.getAction();
9098

91-
if (action instanceof ModelDriven modelDriven) {
99+
if (action instanceof ModelDriven<?> modelDriven) {
92100
ValueStack stack = invocation.getStack();
93101
Object model = modelDriven.getModel();
94-
if (model != null) {
95-
stack.push(model);
102+
if (model != null) {
103+
stack.push(model);
104+
threadAllowlist.allowClassHierarchy(model.getClass());
96105
}
97106
if (refreshModelBeforeResult) {
98107
invocation.addPreResultListener(new RefreshModelBeforeResult(modelDriven, model));

core/src/main/java/org/apache/struts2/interceptor/debugging/DebuggingInterceptor.java

+2-5
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818
*/
1919
package org.apache.struts2.interceptor.debugging;
2020

21-
import jakarta.servlet.http.HttpServletResponse;
22-
import org.apache.commons.lang3.ClassUtils;
2321
import org.apache.logging.log4j.LogManager;
2422
import org.apache.logging.log4j.Logger;
2523
import org.apache.struts2.ActionContext;
@@ -38,6 +36,7 @@
3836
import org.apache.struts2.views.freemarker.FreemarkerManager;
3937
import org.apache.struts2.views.freemarker.FreemarkerResult;
4038

39+
import jakarta.servlet.http.HttpServletResponse;
4140
import java.beans.BeanInfo;
4241
import java.beans.Introspector;
4342
import java.beans.PropertyDescriptor;
@@ -273,9 +272,7 @@ public String intercept(ActionInvocation inv) throws Exception {
273272

274273
private void allowListClass(Object o) {
275274
if (o != null) {
276-
threadAllowlist.allowClass(o.getClass());
277-
ClassUtils.getAllSuperclasses(o.getClass()).forEach(threadAllowlist::allowClass);
278-
ClassUtils.getAllInterfaces(o.getClass()).forEach(threadAllowlist::allowClass);
275+
threadAllowlist.allowClassHierarchy(o.getClass());
279276
}
280277
}
281278

core/src/main/java/org/apache/struts2/interceptor/parameter/ParametersInterceptor.java

+8-11
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
package org.apache.struts2.interceptor.parameter;
2020

2121
import org.apache.commons.lang3.BooleanUtils;
22-
import org.apache.commons.lang3.ClassUtils;
2322
import org.apache.logging.log4j.LogManager;
2423
import org.apache.logging.log4j.Logger;
2524
import org.apache.struts2.ActionContext;
@@ -358,9 +357,8 @@ protected boolean isParameterAnnotatedAndAllowlist(String name, Object action) {
358357
long paramDepth = name.codePoints().mapToObj(c -> (char) c).filter(NESTING_CHARS::contains).count();
359358

360359
if (action instanceof ModelDriven<?> && !ActionContext.getContext().getValueStack().peek().equals(action)) {
361-
LOG.debug("Model driven Action detected, exempting from @StrutsParameter annotation requirement and OGNL allowlisting model type");
362-
// (Exempted by annotation on org.apache.struts2.ModelDriven#getModel)
363-
return hasValidAnnotatedMember("model", action, paramDepth + 1);
360+
LOG.debug("Model driven Action detected, exempting from @StrutsParameter annotation requirement");
361+
return true;
364362
}
365363

366364
if (requireAnnotationsTransitionMode && paramDepth == 0) {
@@ -447,15 +445,13 @@ protected void allowlistParameterizedTypeArg(Type genericType) {
447445
}
448446

449447
protected void allowlistParamType(Type paramType) {
450-
if (paramType instanceof Class) {
451-
allowlistClass((Class<?>) paramType);
448+
if (paramType instanceof Class<?> clazz) {
449+
allowlistClass(clazz);
452450
}
453451
}
454452

455453
protected void allowlistClass(Class<?> clazz) {
456-
threadAllowlist.allowClass(clazz);
457-
ClassUtils.getAllSuperclasses(clazz).forEach(threadAllowlist::allowClass);
458-
ClassUtils.getAllInterfaces(clazz).forEach(threadAllowlist::allowClass);
454+
threadAllowlist.allowClassHierarchy(clazz);
459455
}
460456

461457
protected boolean hasValidAnnotatedField(Object action, String fieldName, long paramDepth) {
@@ -527,10 +523,11 @@ protected Class<?> ultimateClass(Object action) {
527523
}
528524

529525
protected BeanInfo getBeanInfo(Object action) {
526+
Class<?> actionClass = ultimateClass(action);
530527
try {
531-
return ognlUtil.getBeanInfo(ultimateClass(action));
528+
return ognlUtil.getBeanInfo(actionClass);
532529
} catch (IntrospectionException e) {
533-
LOG.warn("Error introspecting Action {} for parameter injection validation", action.getClass(), e);
530+
LOG.warn("Error introspecting Action {} for parameter injection validation", actionClass, e);
534531
return null;
535532
}
536533
}

core/src/main/java/org/apache/struts2/ognl/ThreadAllowlist.java

+11
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
*/
1919
package org.apache.struts2.ognl;
2020

21+
import org.apache.commons.lang3.ClassUtils;
22+
2123
import java.util.HashSet;
2224
import java.util.Set;
2325

@@ -34,6 +36,15 @@ public class ThreadAllowlist {
3436

3537
private final ThreadLocal<Set<Class<?>>> allowlist = new ThreadLocal<>();
3638

39+
/**
40+
* @since 7.1.0
41+
*/
42+
public void allowClassHierarchy(Class<?> clazz) {
43+
allowClass(clazz);
44+
ClassUtils.getAllSuperclasses(clazz).forEach(this::allowClass);
45+
ClassUtils.getAllInterfaces(clazz).forEach(this::allowClass);
46+
}
47+
3748
public void allowClass(Class<?> clazz) {
3849
if (allowlist.get() == null) {
3950
allowlist.set(new HashSet<>());

core/src/test/java/org/apache/struts2/interceptor/ModelDrivenInterceptorTest.java

+8-1
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,19 @@
2020

2121
import com.mockobjects.dynamic.ConstraintMatcher;
2222
import com.mockobjects.dynamic.Mock;
23-
import org.apache.struts2.action.Action;
2423
import org.apache.struts2.ActionContext;
2524
import org.apache.struts2.ActionInvocation;
2625
import org.apache.struts2.ActionSupport;
2726
import org.apache.struts2.ModelDriven;
2827
import org.apache.struts2.XWorkTestCase;
28+
import org.apache.struts2.action.Action;
29+
import org.apache.struts2.ognl.ThreadAllowlist;
2930
import org.apache.struts2.util.ValueStack;
3031

3132
import java.util.Date;
3233

34+
import static org.mockito.Mockito.mock;
35+
import static org.mockito.Mockito.verify;
3336

3437
/**
3538
* @author $Author$
@@ -40,6 +43,7 @@ public class ModelDrivenInterceptorTest extends XWorkTestCase {
4043
Action action;
4144
Mock mockActionInvocation;
4245
ModelDrivenInterceptor modelDrivenInterceptor;
46+
ThreadAllowlist threadAllowlist;
4347
Object model;
4448
PreResultListener preResultListener;
4549
ValueStack stack;
@@ -55,6 +59,7 @@ public void testModelDrivenGetsPushedOntoStack() throws Exception {
5559

5660
Object topOfStack = stack.pop();
5761
assertEquals("our model should be on the top of the stack", model, topOfStack);
62+
verify(threadAllowlist).allowClassHierarchy(model.getClass());
5863
}
5964

6065
private void setupRefreshModelBeforeResult() {
@@ -167,6 +172,8 @@ protected void setUp() throws Exception {
167172
super.setUp();
168173
mockActionInvocation = new Mock(ActionInvocation.class);
169174
modelDrivenInterceptor = new ModelDrivenInterceptor();
175+
threadAllowlist = mock(ThreadAllowlist.class);
176+
modelDrivenInterceptor.setThreadAllowlist(threadAllowlist);
170177
stack = ActionContext.getContext().getValueStack();
171178
model = new Date(); // any object will do
172179
}

core/src/test/java/org/apache/struts2/interceptor/parameter/StrutsParameterAnnotationTest.java

+4-6
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,6 @@ public void publicModelPojo() {
381381

382382
testParameter(action, "name", true);
383383
testParameter(action, "name.nested", true);
384-
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Object.class, Pojo.class));
385384
}
386385

387386
/**
@@ -402,10 +401,9 @@ public void publicModelPojo_proxied() {
402401

403402
testParameter(proxiedAction, "name", true);
404403
testParameter(proxiedAction, "name.nested", true);
405-
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Object.class, Pojo.class));
406404
}
407405

408-
static class FieldAction {
406+
public static class FieldAction {
409407
@StrutsParameter
410408
private String privateStr;
411409

@@ -436,7 +434,7 @@ static class FieldAction {
436434
public Map<String, Pojo> publicPojoMapDepthTwo;
437435
}
438436

439-
static class MethodAction {
437+
public static class MethodAction {
440438

441439
@StrutsParameter
442440
private void setPrivateStr(String str) {
@@ -489,14 +487,14 @@ public Map<String, Pojo> getPublicPojoMapDepthTwo() {
489487
}
490488
}
491489

492-
static class ModelAction implements ModelDriven<Pojo> {
490+
public static class ModelAction implements ModelDriven<Pojo> {
493491

494492
@Override
495493
public Pojo getModel() {
496494
return new Pojo();
497495
}
498496
}
499497

500-
static class Pojo {
498+
public static class Pojo {
501499
}
502500
}

0 commit comments

Comments
 (0)