Skip to content

Commit b4265d3

Browse files
committed
WW-3668 - Vulnerability: User input is evaluated as an OGNL expression when there's a conversion error
git-svn-id: https://svn.apache.org/repos/asf/struts/struts2/trunk@1157009 13f79535-47bb-0310-9956-ffa450edef68
1 parent 6137ac2 commit b4265d3

File tree

6 files changed

+94
-85
lines changed

6 files changed

+94
-85
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ protected Object getOverrideExpr(ActionInvocation invocation, Object value) {
8080
try {
8181
stack.push(value);
8282

83-
return "'" + stack.findValue("top", String.class) + "'";
83+
return escape(stack.findString("top"));
8484
} finally {
8585
stack.pop();
8686
}

core/src/main/resources/template/simple/text.ftl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
maxlength="${parameters.maxlength?html}"<#rt/>
3030
</#if>
3131
<#if parameters.nameValue??>
32-
value="<@s.property value="parameters.nameValue"/>"<#rt/>
32+
value="${parameters.nameValue?html}"<#rt/>
3333
</#if>
3434
<#if parameters.disabled?default(false)>
3535
disabled="disabled"<#rt/>

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

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,17 @@
2121

2222
package org.apache.struts2.interceptor;
2323

24-
import java.util.HashMap;
25-
import java.util.Map;
26-
27-
import org.apache.struts2.StrutsTestCase;
28-
2924
import com.mockobjects.dynamic.C;
3025
import com.mockobjects.dynamic.Mock;
3126
import com.opensymphony.xwork2.Action;
3227
import com.opensymphony.xwork2.ActionContext;
3328
import com.opensymphony.xwork2.ActionInvocation;
3429
import com.opensymphony.xwork2.ActionSupport;
3530
import com.opensymphony.xwork2.util.ValueStack;
36-
import com.opensymphony.xwork2.util.ValueStackFactory;
31+
import org.apache.struts2.StrutsTestCase;
32+
33+
import java.util.HashMap;
34+
import java.util.Map;
3735

3836

3937
/**
@@ -44,14 +42,14 @@ public class StrutsConversionErrorInterceptorTest extends StrutsTestCase {
4442

4543
protected ActionContext context;
4644
protected ActionInvocation invocation;
47-
protected Map conversionErrors;
45+
protected Map<String, Object> conversionErrors;
4846
protected Mock mockInvocation;
4947
protected ValueStack stack;
5048
protected StrutsConversionErrorInterceptor interceptor;
5149

5250

5351
public void testEmptyValuesDoNotSetFieldErrors() throws Exception {
54-
conversionErrors.put("foo", new Long(123));
52+
conversionErrors.put("foo", 123L);
5553
conversionErrors.put("bar", "");
5654
conversionErrors.put("baz", new String[]{""});
5755

@@ -70,7 +68,7 @@ public void testEmptyValuesDoNotSetFieldErrors() throws Exception {
7068
}
7169

7270
public void testFieldErrorAdded() throws Exception {
73-
conversionErrors.put("foo", new Long(123));
71+
conversionErrors.put("foo", 123L);
7472

7573
ActionSupport action = new ActionSupport();
7674
mockInvocation.expectAndReturn("getAction", action);
@@ -89,7 +87,7 @@ protected void setUp() throws Exception {
8987
invocation = (ActionInvocation) mockInvocation.proxy();
9088
stack = ActionContext.getContext().getValueStack();
9189
context = new ActionContext(stack.getContext());
92-
conversionErrors = new HashMap();
90+
conversionErrors = new HashMap<String, Object>();
9391
context.setConversionErrors(conversionErrors);
9492
mockInvocation.matchAndReturn("getInvocationContext", context);
9593
mockInvocation.expectAndReturn("invoke", Action.SUCCESS);

xwork-core/src/main/java/com/opensymphony/xwork2/interceptor/ConversionErrorInterceptor.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.opensymphony.xwork2.ValidationAware;
2121
import com.opensymphony.xwork2.conversion.impl.XWorkConverter;
2222
import com.opensymphony.xwork2.util.ValueStack;
23+
import org.apache.commons.lang.StringEscapeUtils;
2324

2425
import java.util.HashMap;
2526
import java.util.Map;
@@ -84,7 +85,11 @@ public class ConversionErrorInterceptor extends AbstractInterceptor {
8485
public static final String ORIGINAL_PROPERTY_OVERRIDE = "original.property.override";
8586

8687
protected Object getOverrideExpr(ActionInvocation invocation, Object value) {
87-
return "'" + value + "'";
88+
return escape(value);
89+
}
90+
91+
protected String escape(Object value) {
92+
return "\"" + StringEscapeUtils.escapeJava(String.valueOf(value)) + "\"";
8893
}
8994

9095
@Override

xwork-core/src/main/java/com/opensymphony/xwork2/validator/validators/RepopulateConversionErrorFieldValidatorSupport.java

Lines changed: 73 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -22,58 +22,59 @@
2222
import com.opensymphony.xwork2.util.logging.Logger;
2323
import com.opensymphony.xwork2.util.logging.LoggerFactory;
2424
import com.opensymphony.xwork2.validator.ValidationException;
25+
import org.apache.commons.lang.StringEscapeUtils;
2526

2627
import java.util.LinkedHashMap;
2728
import java.util.Map;
2829

2930
/**
30-
*
31-
*
31+
*
32+
*
3233
* An abstract base class that adds in the capability to populate the stack with
3334
* a fake parameter map when a conversion error has occurred and the 'repopulateField'
3435
* property is set to "true".
35-
*
36+
*
3637
* <p/>
37-
*
38+
*
3839
*
3940
* <!-- START SNIPPET: javadoc -->
4041
*
41-
* The capability of auto-repopulating the stack with a fake parameter map when
42-
* a conversion error has occurred can be done with 'repopulateField' property
43-
* set to "true".
42+
* The capability of auto-repopulating the stack with a fake parameter map when
43+
* a conversion error has occurred can be done with 'repopulateField' property
44+
* set to "true".
4445
*
4546
* <p/>
4647
*
47-
* This is typically usefull when one wants to repopulate the field with the original value
48-
* when a conversion error occurred. Eg. with a textfield that only allows an Integer
48+
* This is typically usefull when one wants to repopulate the field with the original value
49+
* when a conversion error occurred. Eg. with a textfield that only allows an Integer
4950
* (the action class have an Integer field declared), upon conversion error, the incorrectly
5051
* entered integer (maybe a text 'one') will not appear when dispatched back. With 'repopulateField'
51-
* porperty set to true, it will, meaning the textfield will have 'one' as its value
52+
* porperty set to true, it will, meaning the textfield will have 'one' as its value
5253
* upon conversion error.
53-
*
54+
*
5455
* <!-- END SNIPPET: javadoc -->
55-
*
56+
*
5657
* <p/>
57-
*
58+
*
5859
* <pre>
5960
* <!-- START SNIPPET: exampleJspPage -->
60-
*
61+
*
6162
* &lt;!-- myJspPage.jsp --&gt;
6263
* &lt;ww:form action="someAction" method="POST"&gt;
6364
* ....
64-
* &lt;ww:textfield
65+
* &lt;ww:textfield
6566
* label="My Integer Field"
6667
* name="myIntegerField" /&gt;
6768
* ....
68-
* &lt;ww:submit /&gt;
69+
* &lt;ww:submit /&gt;
6970
* &lt;/ww:form&gt;
70-
*
71+
*
7172
* <!-- END SNIPPET: exampleJspPage -->
7273
* </pre>
73-
*
74+
*
7475
* <pre>
7576
* <!-- START SNIPPET: exampleXwork -->
76-
*
77+
*
7778
* &lt;!-- xwork.xml --&gt;
7879
* &lt;xwork&gt;
7980
* &lt;include file="xwork-default.xml" /&gt;
@@ -88,31 +89,31 @@
8889
* &lt;/package&gt;
8990
* ....
9091
* &lt;/xwork&gt;
91-
*
92+
*
9293
* <!-- END SNIPPET:exampleXwork -->
9394
* </pre>
94-
*
95-
*
95+
*
96+
*
9697
* <pre>
9798
* <!-- START SNIPPET: exampleJava -->
98-
*
99+
*
99100
* &lt;!-- MyActionSupport.java --&gt;
100101
* public class MyActionSupport extends ActionSupport {
101102
* private Integer myIntegerField;
102-
*
103+
*
103104
* public Integer getMyIntegerField() { return this.myIntegerField; }
104-
* public void setMyIntegerField(Integer myIntegerField) {
105-
* this.myIntegerField = myIntegerField;
105+
* public void setMyIntegerField(Integer myIntegerField) {
106+
* this.myIntegerField = myIntegerField;
106107
* }
107108
* }
108-
*
109+
*
109110
* <!-- END SNIPPET: exampleJava -->
110111
* </pre>
111-
*
112-
*
112+
*
113+
*
113114
* <pre>
114115
* <!-- START SNIPPET: exampleValidation -->
115-
*
116+
*
116117
* &lt;!-- MyActionSupport-someAction-validation.xml --&gt;
117118
* &lt;validators&gt;
118119
* ...
@@ -124,43 +125,43 @@
124125
* &lt;/field&gt;
125126
* ...
126127
* &lt;/validators&gt;
127-
*
128+
*
128129
* <!-- END SNIPPET: exampleValidation -->
129130
* </pre>
130-
*
131+
*
131132
* @author tm_jee
132133
* @version $Date$ $Id$
133134
*/
134135
public abstract class RepopulateConversionErrorFieldValidatorSupport extends FieldValidatorSupport {
135-
136-
private static final Logger LOG = LoggerFactory.getLogger(RepopulateConversionErrorFieldValidatorSupport.class);
137-
138-
private String repopulateFieldAsString = "false";
139-
private boolean repopulateFieldAsBoolean = false;
140-
141-
public String getRepopulateField() {
142-
return repopulateFieldAsString;
143-
}
144-
145-
public void setRepopulateField(String repopulateField) {
146-
this.repopulateFieldAsString = repopulateField == null ? repopulateField : repopulateField.trim();
147-
this.repopulateFieldAsBoolean = "true".equalsIgnoreCase(this.repopulateFieldAsString) ? (true) : (false);
148-
}
149-
150-
public void validate(Object object) throws ValidationException {
151-
doValidate(object);
152-
if (repopulateFieldAsBoolean) {
153-
repopulateField(object);
154-
}
155-
}
156-
157-
public void repopulateField(Object object) throws ValidationException {
158-
159-
ActionInvocation invocation = ActionContext.getContext().getActionInvocation();
160-
Map<String, Object> conversionErrors = ActionContext.getContext().getConversionErrors();
161-
162-
String fieldName = getFieldName();
163-
String fullFieldName = getValidatorContext().getFullFieldName(fieldName);
136+
137+
private static final Logger LOG = LoggerFactory.getLogger(RepopulateConversionErrorFieldValidatorSupport.class);
138+
139+
private String repopulateFieldAsString = "false";
140+
private boolean repopulateFieldAsBoolean = false;
141+
142+
public String getRepopulateField() {
143+
return repopulateFieldAsString;
144+
}
145+
146+
public void setRepopulateField(String repopulateField) {
147+
this.repopulateFieldAsString = repopulateField == null ? repopulateField : repopulateField.trim();
148+
this.repopulateFieldAsBoolean = "true".equalsIgnoreCase(this.repopulateFieldAsString) ? (true) : (false);
149+
}
150+
151+
public void validate(Object object) throws ValidationException {
152+
doValidate(object);
153+
if (repopulateFieldAsBoolean) {
154+
repopulateField(object);
155+
}
156+
}
157+
158+
public void repopulateField(Object object) throws ValidationException {
159+
160+
ActionInvocation invocation = ActionContext.getContext().getActionInvocation();
161+
Map<String, Object> conversionErrors = ActionContext.getContext().getConversionErrors();
162+
163+
String fieldName = getFieldName();
164+
String fullFieldName = getValidatorContext().getFullFieldName(fieldName);
164165
if (conversionErrors.containsKey(fullFieldName)) {
165166
Object value = conversionErrors.get(fullFieldName);
166167

@@ -170,18 +171,18 @@ public void repopulateField(Object object) throws ValidationException {
170171
if (value instanceof String[]) {
171172
// take the first element, if possible
172173
String[] tmpValue = (String[]) value;
173-
if (tmpValue != null && (tmpValue.length > 0)) {
174+
if ((tmpValue.length > 0)) {
174175
doExprOverride = true;
175-
fakeParams.put(fullFieldName, "'" + tmpValue[0] + "'");
176+
fakeParams.put(fullFieldName, escape(tmpValue[0]));
176177
} else {
177178
if (LOG.isWarnEnabled()) {
178-
LOG.warn("value is an empty array of String or with first element in it as null [" + value + "], will not repopulate conversion error ");
179+
LOG.warn("value is an empty array of String or with first element in it as null [" + value + "], will not repopulate conversion error ");
179180
}
180181
}
181182
} else if (value instanceof String) {
182183
String tmpValue = (String) value;
183184
doExprOverride = true;
184-
fakeParams.put(fullFieldName, "'" + tmpValue + "'");
185+
fakeParams.put(fullFieldName, escape(tmpValue));
185186
} else {
186187
// opps... it should be
187188
if (LOG.isWarnEnabled()) {
@@ -198,7 +199,11 @@ public void beforeResult(ActionInvocation invocation, String resultCode) {
198199
});
199200
}
200201
}
201-
}
202-
203-
protected abstract void doValidate(Object object) throws ValidationException;
202+
}
203+
204+
protected String escape(String value) {
205+
return "\"" + StringEscapeUtils.escapeJava(value) + "\"";
206+
}
207+
208+
protected abstract void doValidate(Object object) throws ValidationException;
204209
}

xwork-core/src/test/java/com/opensymphony/xwork2/interceptor/ConversionErrorInterceptorTest.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public class ConversionErrorInterceptorTest extends XWorkTestCase {
3535
protected ActionContext context;
3636
protected ActionInvocation invocation;
3737
protected ConversionErrorInterceptor interceptor;
38-
protected Map<String,Object> conversionErrors;
38+
protected Map<String, Object> conversionErrors;
3939
protected Mock mockInvocation;
4040
protected ValueStack stack;
4141

@@ -86,10 +86,11 @@ public void testWithPreResultListener() throws Exception {
8686

8787
/**
8888
* See WW-3668
89+
*
8990
* @throws Exception
9091
*/
9192
public void testWithPreResultListenerAgainstMaliciousCode() throws Exception {
92-
conversionErrors.put("foo", "' + #root + '");
93+
conversionErrors.put("foo", "\" + #root + \"");
9394

9495
ActionContext ac = createActionContext();
9596

@@ -104,7 +105,7 @@ public void testWithPreResultListenerAgainstMaliciousCode() throws Exception {
104105
assertTrue(action.hasFieldErrors());
105106
assertNotNull(action.getFieldErrors().get("foo"));
106107

107-
assertEquals("' + #root + '", stack.findValue("foo"));
108+
assertEquals("\" + #root + \"", stack.findValue("foo"));
108109
}
109110

110111
private MockActionInvocation createActionInvocation(ActionContext ac) {
@@ -137,7 +138,7 @@ protected void setUp() throws Exception {
137138
invocation = (ActionInvocation) mockInvocation.proxy();
138139
stack = ActionContext.getContext().getValueStack();
139140
context = new ActionContext(stack.getContext());
140-
conversionErrors = new HashMap<String,Object>();
141+
conversionErrors = new HashMap<String, Object>();
141142
context.setConversionErrors(conversionErrors);
142143
mockInvocation.matchAndReturn("getInvocationContext", context);
143144
mockInvocation.expect("addPreResultListener", C.isA(PreResultListener.class));

0 commit comments

Comments
 (0)