High severityNVD Advisory· Published Mar 2, 2012· Updated Apr 29, 2026
CVE-2012-0838
CVE-2012-0838
Description
Apache Struts 2 before 2.2.3.1 evaluates a string as an OGNL expression during the handling of a conversion error, which allows remote attackers to modify run-time data values, and consequently execute arbitrary code, via invalid input to a field.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.apache.struts:struts2-coreMaven | < 2.2.3.1 | 2.2.3.1 |
org.apache.struts.xwork:xwork-coreMaven | < 2.2.3.1 | 2.2.3.1 |
Affected products
3- ghsa-coords2 versions
< 2.2.3.1+ 1 more
- (no CPE)range: < 2.2.3.1
- (no CPE)range: < 2.2.3.1
Patches
325e50069d604Security patch applied
6 files changed · +128 −77
core/src/main/java/org/apache/struts2/interceptor/StrutsConversionErrorInterceptor.java+1 −1 modified@@ -80,7 +80,7 @@ protected Object getOverrideExpr(ActionInvocation invocation, Object value) { try { stack.push(value); - return "'" + stack.findValue("top", String.class) + "'"; + return escape(stack.findString("top")); } finally { stack.pop(); }
core/src/main/resources/template/simple/text.ftl+2 −2 modified@@ -29,7 +29,7 @@ maxlength="${parameters.maxlength?html}"<#rt/> </#if> <#if parameters.nameValue??> - value="<@s.property value="parameters.nameValue"/>"<#rt/> + value="${parameters.nameValue?html}"<#rt/> </#if> <#if parameters.disabled?default(false)> disabled="disabled"<#rt/> @@ -50,4 +50,4 @@ <#include "/${parameters.templateDir}/simple/scripting-events.ftl" /> <#include "/${parameters.templateDir}/simple/common-attributes.ftl" /> <#include "/${parameters.templateDir}/simple/dynamic-attributes.ftl" /> -/> \ No newline at end of file +/>
core/src/test/java/org/apache/struts2/interceptor/StrutsConversionErrorInterceptorTest.java+8 −10 modified@@ -21,19 +21,17 @@ package org.apache.struts2.interceptor; -import java.util.HashMap; -import java.util.Map; - -import org.apache.struts2.StrutsTestCase; - import com.mockobjects.dynamic.C; import com.mockobjects.dynamic.Mock; import com.opensymphony.xwork2.Action; import com.opensymphony.xwork2.ActionContext; import com.opensymphony.xwork2.ActionInvocation; import com.opensymphony.xwork2.ActionSupport; import com.opensymphony.xwork2.util.ValueStack; -import com.opensymphony.xwork2.util.ValueStackFactory; +import org.apache.struts2.StrutsTestCase; + +import java.util.HashMap; +import java.util.Map; /** @@ -44,14 +42,14 @@ public class StrutsConversionErrorInterceptorTest extends StrutsTestCase { protected ActionContext context; protected ActionInvocation invocation; - protected Map conversionErrors; + protected Map<String, Object> conversionErrors; protected Mock mockInvocation; protected ValueStack stack; protected StrutsConversionErrorInterceptor interceptor; public void testEmptyValuesDoNotSetFieldErrors() throws Exception { - conversionErrors.put("foo", new Long(123)); + conversionErrors.put("foo", 123L); conversionErrors.put("bar", ""); conversionErrors.put("baz", new String[]{""}); @@ -70,7 +68,7 @@ public void testEmptyValuesDoNotSetFieldErrors() throws Exception { } public void testFieldErrorAdded() throws Exception { - conversionErrors.put("foo", new Long(123)); + conversionErrors.put("foo", 123L); ActionSupport action = new ActionSupport(); mockInvocation.expectAndReturn("getAction", action); @@ -89,7 +87,7 @@ protected void setUp() throws Exception { invocation = (ActionInvocation) mockInvocation.proxy(); stack = ActionContext.getContext().getValueStack(); context = new ActionContext(stack.getContext()); - conversionErrors = new HashMap(); + conversionErrors = new HashMap<String, Object>(); context.setConversionErrors(conversionErrors); mockInvocation.matchAndReturn("getInvocationContext", context); mockInvocation.expectAndReturn("invoke", Action.SUCCESS);
xwork-core/src/main/java/com/opensymphony/xwork2/interceptor/ConversionErrorInterceptor.java+6 −1 modified@@ -20,6 +20,7 @@ import com.opensymphony.xwork2.ValidationAware; import com.opensymphony.xwork2.conversion.impl.XWorkConverter; import com.opensymphony.xwork2.util.ValueStack; +import org.apache.commons.lang.StringEscapeUtils; import java.util.HashMap; import java.util.Map; @@ -84,7 +85,11 @@ public class ConversionErrorInterceptor extends AbstractInterceptor { public static final String ORIGINAL_PROPERTY_OVERRIDE = "original.property.override"; protected Object getOverrideExpr(ActionInvocation invocation, Object value) { - return "'" + value + "'"; + return escape(value); + } + + protected String escape(Object value) { + return "\"" + StringEscapeUtils.escapeJava(String.valueOf(value)) + "\""; } @Override
xwork-core/src/main/java/com/opensymphony/xwork2/validator/validators/RepopulateConversionErrorFieldValidatorSupport.java+58 −49 modified@@ -22,58 +22,59 @@ import com.opensymphony.xwork2.util.logging.Logger; import com.opensymphony.xwork2.util.logging.LoggerFactory; import com.opensymphony.xwork2.validator.ValidationException; +import org.apache.commons.lang.StringEscapeUtils; import java.util.LinkedHashMap; import java.util.Map; /** - * - * + * + * * An abstract base class that adds in the capability to populate the stack with * a fake parameter map when a conversion error has occurred and the 'repopulateField' * property is set to "true". - * + * * <p/> - * + * * * <!-- START SNIPPET: javadoc --> * - * The capability of auto-repopulating the stack with a fake parameter map when - * a conversion error has occurred can be done with 'repopulateField' property - * set to "true". + * The capability of auto-repopulating the stack with a fake parameter map when + * a conversion error has occurred can be done with 'repopulateField' property + * set to "true". * * <p/> * - * This is typically usefull when one wants to repopulate the field with the original value - * when a conversion error occurred. Eg. with a textfield that only allows an Integer + * This is typically usefull when one wants to repopulate the field with the original value + * when a conversion error occurred. Eg. with a textfield that only allows an Integer * (the action class have an Integer field declared), upon conversion error, the incorrectly * entered integer (maybe a text 'one') will not appear when dispatched back. With 'repopulateField' - * porperty set to true, it will, meaning the textfield will have 'one' as its value + * porperty set to true, it will, meaning the textfield will have 'one' as its value * upon conversion error. - * + * * <!-- END SNIPPET: javadoc --> - * + * * <p/> - * + * * <pre> * <!-- START SNIPPET: exampleJspPage --> - * + * * <!-- myJspPage.jsp --> * <ww:form action="someAction" method="POST"> * .... - * <ww:textfield + * <ww:textfield * label="My Integer Field" * name="myIntegerField" /> * .... - * <ww:submit /> + * <ww:submit /> * </ww:form> - * + * * <!-- END SNIPPET: exampleJspPage --> * </pre> - * + * * <pre> * <!-- START SNIPPET: exampleXwork --> - * + * * <!-- xwork.xml --> * <xwork> * <include file="xwork-default.xml" /> @@ -88,31 +89,31 @@ * </package> * .... * </xwork> - * + * * <!-- END SNIPPET:exampleXwork --> * </pre> - * - * + * + * * <pre> * <!-- START SNIPPET: exampleJava --> - * + * * <!-- MyActionSupport.java --> * public class MyActionSupport extends ActionSupport { * private Integer myIntegerField; - * + * * public Integer getMyIntegerField() { return this.myIntegerField; } - * public void setMyIntegerField(Integer myIntegerField) { - * this.myIntegerField = myIntegerField; + * public void setMyIntegerField(Integer myIntegerField) { + * this.myIntegerField = myIntegerField; * } * } - * + * * <!-- END SNIPPET: exampleJava --> * </pre> - * - * + * + * * <pre> * <!-- START SNIPPET: exampleValidation --> - * + * * <!-- MyActionSupport-someAction-validation.xml --> * <validators> * ... @@ -124,24 +125,24 @@ * </field> * ... * </validators> - * + * * <!-- END SNIPPET: exampleValidation --> * </pre> - * + * * @author tm_jee * @version $Date$ $Id$ */ public abstract class RepopulateConversionErrorFieldValidatorSupport extends FieldValidatorSupport { - + private static final Logger LOG = LoggerFactory.getLogger(RepopulateConversionErrorFieldValidatorSupport.class); - + private String repopulateFieldAsString = "false"; private boolean repopulateFieldAsBoolean = false; - - public String getRepopulateField() { + + public String getRepopulateField() { return repopulateFieldAsString; } - + public void setRepopulateField(String repopulateField) { this.repopulateFieldAsString = repopulateField == null ? repopulateField : repopulateField.trim(); this.repopulateFieldAsBoolean = "true".equalsIgnoreCase(this.repopulateFieldAsString) ? (true) : (false); @@ -153,12 +154,12 @@ public void validate(Object object) throws ValidationException { repopulateField(object); } } - + public void repopulateField(Object object) throws ValidationException { - + ActionInvocation invocation = ActionContext.getContext().getActionInvocation(); Map<String, Object> conversionErrors = ActionContext.getContext().getConversionErrors(); - + String fieldName = getFieldName(); String fullFieldName = getValidatorContext().getFullFieldName(fieldName); if (conversionErrors.containsKey(fullFieldName)) { @@ -170,19 +171,23 @@ public void repopulateField(Object object) throws ValidationException { if (value instanceof String[]) { // take the first element, if possible String[] tmpValue = (String[]) value; - if (tmpValue != null && (tmpValue.length > 0)) { + if ((tmpValue.length > 0)) { doExprOverride = true; - fakeParams.put(fullFieldName, "'" + tmpValue[0] + "'"); + fakeParams.put(fullFieldName, escape(tmpValue[0])); } else { - LOG.warn("value is an empty array of String or with first element in it as null [" + value + "], will not repopulate conversion error "); + if (LOG.isWarnEnabled()) { + LOG.warn("value is an empty array of String or with first element in it as null [" + value + "], will not repopulate conversion error "); + } } } else if (value instanceof String) { String tmpValue = (String) value; doExprOverride = true; - fakeParams.put(fullFieldName, "'" + tmpValue + "'"); + fakeParams.put(fullFieldName, escape(tmpValue)); } else { // opps... it should be - LOG.warn("conversion error value is not a String or array of String but instead is [" + value + "], will not repopulate conversion error"); + if (LOG.isWarnEnabled()) { + LOG.warn("conversion error value is not a String or array of String but instead is [" + value + "], will not repopulate conversion error"); + } } if (doExprOverride) { @@ -194,7 +199,11 @@ public void beforeResult(ActionInvocation invocation, String resultCode) { }); } } - } - - protected abstract void doValidate(Object object) throws ValidationException; -} + } + + protected String escape(String value) { + return "\"" + StringEscapeUtils.escapeJava(value) + "\""; + } + + protected abstract void doValidate(Object object) throws ValidationException; +} \ No newline at end of file
xwork-core/src/test/java/com/opensymphony/xwork2/interceptor/ConversionErrorInterceptorTest.java+53 −14 modified@@ -35,13 +35,13 @@ public class ConversionErrorInterceptorTest extends XWorkTestCase { protected ActionContext context; protected ActionInvocation invocation; protected ConversionErrorInterceptor interceptor; - protected Map conversionErrors; + protected Map<String, Object> conversionErrors; protected Mock mockInvocation; protected ValueStack stack; public void testFieldErrorAdded() throws Exception { - conversionErrors.put("foo", new Long(123)); + conversionErrors.put("foo", 123L); SimpleAction action = new SimpleAction(); mockInvocation.expectAndReturn("getAction", action); @@ -69,19 +69,12 @@ public void testFieldErrorWithMapKeyAdded() throws Exception { public void testWithPreResultListener() throws Exception { conversionErrors.put("foo", "Hello"); - ActionContext ac = new ActionContext(stack.getContext()); - ac.setConversionErrors(conversionErrors); - ac.setValueStack(stack); + ActionContext ac = createActionContext(); + MockActionInvocation mai = createActionInvocation(ac); + SimpleAction action = createAction(mai); - MockActionInvocation mai = new MockActionInvocation(); - mai.setInvocationContext(ac); - mai.setStack(stack); - SimpleAction action = new SimpleAction(); - action.setFoo(55); - mai.setAction(action); - stack.push(action); assertNull(action.getFieldErrors().get("foo")); - assertEquals(new Integer(55), stack.findValue("foo")); + assertEquals(55, stack.findValue("foo")); interceptor.intercept(mai); @@ -91,6 +84,52 @@ public void testWithPreResultListener() throws Exception { assertEquals("Hello", stack.findValue("foo")); // assume that the original value is reset } + /** + * See WW-3668 + * + * @throws Exception + */ + public void testWithPreResultListenerAgainstMaliciousCode() throws Exception { + conversionErrors.put("foo", "\" + #root + \""); + + ActionContext ac = createActionContext(); + + MockActionInvocation mai = createActionInvocation(ac); + + SimpleAction action = createAction(mai); + assertNull(action.getFieldErrors().get("foo")); + assertEquals(55, stack.findValue("foo")); + + interceptor.intercept(mai); + + assertTrue(action.hasFieldErrors()); + assertNotNull(action.getFieldErrors().get("foo")); + + assertEquals("\" + #root + \"", stack.findValue("foo")); + } + + private MockActionInvocation createActionInvocation(ActionContext ac) { + MockActionInvocation mai = new MockActionInvocation(); + mai.setInvocationContext(ac); + mai.setStack(stack); + return mai; + } + + private SimpleAction createAction(MockActionInvocation mai) { + SimpleAction action = new SimpleAction(); + action.setFoo(55); + mai.setAction(action); + stack.push(action); + return action; + } + + private ActionContext createActionContext() { + ActionContext ac = new ActionContext(stack.getContext()); + ac.setConversionErrors(conversionErrors); + ac.setValueStack(stack); + return ac; + } + @Override protected void setUp() throws Exception { super.setUp(); @@ -99,7 +138,7 @@ protected void setUp() throws Exception { invocation = (ActionInvocation) mockInvocation.proxy(); stack = ActionContext.getContext().getValueStack(); context = new ActionContext(stack.getContext()); - conversionErrors = new HashMap(); + conversionErrors = new HashMap<String, Object>(); context.setConversionErrors(conversionErrors); mockInvocation.matchAndReturn("getInvocationContext", context); mockInvocation.expect("addPreResultListener", C.isA(PreResultListener.class));
b4265d369dc2WW-3668 - Vulnerability: User input is evaluated as an OGNL expression when there's a conversion error
6 files changed · +94 −85
core/src/main/java/org/apache/struts2/interceptor/StrutsConversionErrorInterceptor.java+1 −1 modified@@ -80,7 +80,7 @@ protected Object getOverrideExpr(ActionInvocation invocation, Object value) { try { stack.push(value); - return "'" + stack.findValue("top", String.class) + "'"; + return escape(stack.findString("top")); } finally { stack.pop(); }
core/src/main/resources/template/simple/text.ftl+1 −1 modified@@ -29,7 +29,7 @@ maxlength="${parameters.maxlength?html}"<#rt/> </#if> <#if parameters.nameValue??> - value="<@s.property value="parameters.nameValue"/>"<#rt/> + value="${parameters.nameValue?html}"<#rt/> </#if> <#if parameters.disabled?default(false)> disabled="disabled"<#rt/>
core/src/test/java/org/apache/struts2/interceptor/StrutsConversionErrorInterceptorTest.java+8 −10 modified@@ -21,19 +21,17 @@ package org.apache.struts2.interceptor; -import java.util.HashMap; -import java.util.Map; - -import org.apache.struts2.StrutsTestCase; - import com.mockobjects.dynamic.C; import com.mockobjects.dynamic.Mock; import com.opensymphony.xwork2.Action; import com.opensymphony.xwork2.ActionContext; import com.opensymphony.xwork2.ActionInvocation; import com.opensymphony.xwork2.ActionSupport; import com.opensymphony.xwork2.util.ValueStack; -import com.opensymphony.xwork2.util.ValueStackFactory; +import org.apache.struts2.StrutsTestCase; + +import java.util.HashMap; +import java.util.Map; /** @@ -44,14 +42,14 @@ public class StrutsConversionErrorInterceptorTest extends StrutsTestCase { protected ActionContext context; protected ActionInvocation invocation; - protected Map conversionErrors; + protected Map<String, Object> conversionErrors; protected Mock mockInvocation; protected ValueStack stack; protected StrutsConversionErrorInterceptor interceptor; public void testEmptyValuesDoNotSetFieldErrors() throws Exception { - conversionErrors.put("foo", new Long(123)); + conversionErrors.put("foo", 123L); conversionErrors.put("bar", ""); conversionErrors.put("baz", new String[]{""}); @@ -70,7 +68,7 @@ public void testEmptyValuesDoNotSetFieldErrors() throws Exception { } public void testFieldErrorAdded() throws Exception { - conversionErrors.put("foo", new Long(123)); + conversionErrors.put("foo", 123L); ActionSupport action = new ActionSupport(); mockInvocation.expectAndReturn("getAction", action); @@ -89,7 +87,7 @@ protected void setUp() throws Exception { invocation = (ActionInvocation) mockInvocation.proxy(); stack = ActionContext.getContext().getValueStack(); context = new ActionContext(stack.getContext()); - conversionErrors = new HashMap(); + conversionErrors = new HashMap<String, Object>(); context.setConversionErrors(conversionErrors); mockInvocation.matchAndReturn("getInvocationContext", context); mockInvocation.expectAndReturn("invoke", Action.SUCCESS);
xwork-core/src/main/java/com/opensymphony/xwork2/interceptor/ConversionErrorInterceptor.java+6 −1 modified@@ -20,6 +20,7 @@ import com.opensymphony.xwork2.ValidationAware; import com.opensymphony.xwork2.conversion.impl.XWorkConverter; import com.opensymphony.xwork2.util.ValueStack; +import org.apache.commons.lang.StringEscapeUtils; import java.util.HashMap; import java.util.Map; @@ -84,7 +85,11 @@ public class ConversionErrorInterceptor extends AbstractInterceptor { public static final String ORIGINAL_PROPERTY_OVERRIDE = "original.property.override"; protected Object getOverrideExpr(ActionInvocation invocation, Object value) { - return "'" + value + "'"; + return escape(value); + } + + protected String escape(Object value) { + return "\"" + StringEscapeUtils.escapeJava(String.valueOf(value)) + "\""; } @Override
xwork-core/src/main/java/com/opensymphony/xwork2/validator/validators/RepopulateConversionErrorFieldValidatorSupport.java+73 −68 modified@@ -22,58 +22,59 @@ import com.opensymphony.xwork2.util.logging.Logger; import com.opensymphony.xwork2.util.logging.LoggerFactory; import com.opensymphony.xwork2.validator.ValidationException; +import org.apache.commons.lang.StringEscapeUtils; import java.util.LinkedHashMap; import java.util.Map; /** - * - * + * + * * An abstract base class that adds in the capability to populate the stack with * a fake parameter map when a conversion error has occurred and the 'repopulateField' * property is set to "true". - * + * * <p/> - * + * * * <!-- START SNIPPET: javadoc --> * - * The capability of auto-repopulating the stack with a fake parameter map when - * a conversion error has occurred can be done with 'repopulateField' property - * set to "true". + * The capability of auto-repopulating the stack with a fake parameter map when + * a conversion error has occurred can be done with 'repopulateField' property + * set to "true". * * <p/> * - * This is typically usefull when one wants to repopulate the field with the original value - * when a conversion error occurred. Eg. with a textfield that only allows an Integer + * This is typically usefull when one wants to repopulate the field with the original value + * when a conversion error occurred. Eg. with a textfield that only allows an Integer * (the action class have an Integer field declared), upon conversion error, the incorrectly * entered integer (maybe a text 'one') will not appear when dispatched back. With 'repopulateField' - * porperty set to true, it will, meaning the textfield will have 'one' as its value + * porperty set to true, it will, meaning the textfield will have 'one' as its value * upon conversion error. - * + * * <!-- END SNIPPET: javadoc --> - * + * * <p/> - * + * * <pre> * <!-- START SNIPPET: exampleJspPage --> - * + * * <!-- myJspPage.jsp --> * <ww:form action="someAction" method="POST"> * .... - * <ww:textfield + * <ww:textfield * label="My Integer Field" * name="myIntegerField" /> * .... - * <ww:submit /> + * <ww:submit /> * </ww:form> - * + * * <!-- END SNIPPET: exampleJspPage --> * </pre> - * + * * <pre> * <!-- START SNIPPET: exampleXwork --> - * + * * <!-- xwork.xml --> * <xwork> * <include file="xwork-default.xml" /> @@ -88,31 +89,31 @@ * </package> * .... * </xwork> - * + * * <!-- END SNIPPET:exampleXwork --> * </pre> - * - * + * + * * <pre> * <!-- START SNIPPET: exampleJava --> - * + * * <!-- MyActionSupport.java --> * public class MyActionSupport extends ActionSupport { * private Integer myIntegerField; - * + * * public Integer getMyIntegerField() { return this.myIntegerField; } - * public void setMyIntegerField(Integer myIntegerField) { - * this.myIntegerField = myIntegerField; + * public void setMyIntegerField(Integer myIntegerField) { + * this.myIntegerField = myIntegerField; * } * } - * + * * <!-- END SNIPPET: exampleJava --> * </pre> - * - * + * + * * <pre> * <!-- START SNIPPET: exampleValidation --> - * + * * <!-- MyActionSupport-someAction-validation.xml --> * <validators> * ... @@ -124,43 +125,43 @@ * </field> * ... * </validators> - * + * * <!-- END SNIPPET: exampleValidation --> * </pre> - * + * * @author tm_jee * @version $Date$ $Id$ */ public abstract class RepopulateConversionErrorFieldValidatorSupport extends FieldValidatorSupport { - - private static final Logger LOG = LoggerFactory.getLogger(RepopulateConversionErrorFieldValidatorSupport.class); - - private String repopulateFieldAsString = "false"; - private boolean repopulateFieldAsBoolean = false; - - public String getRepopulateField() { - return repopulateFieldAsString; - } - - public void setRepopulateField(String repopulateField) { - this.repopulateFieldAsString = repopulateField == null ? repopulateField : repopulateField.trim(); - this.repopulateFieldAsBoolean = "true".equalsIgnoreCase(this.repopulateFieldAsString) ? (true) : (false); - } - - public void validate(Object object) throws ValidationException { - doValidate(object); - if (repopulateFieldAsBoolean) { - repopulateField(object); - } - } - - public void repopulateField(Object object) throws ValidationException { - - ActionInvocation invocation = ActionContext.getContext().getActionInvocation(); - Map<String, Object> conversionErrors = ActionContext.getContext().getConversionErrors(); - - String fieldName = getFieldName(); - String fullFieldName = getValidatorContext().getFullFieldName(fieldName); + + private static final Logger LOG = LoggerFactory.getLogger(RepopulateConversionErrorFieldValidatorSupport.class); + + private String repopulateFieldAsString = "false"; + private boolean repopulateFieldAsBoolean = false; + + public String getRepopulateField() { + return repopulateFieldAsString; + } + + public void setRepopulateField(String repopulateField) { + this.repopulateFieldAsString = repopulateField == null ? repopulateField : repopulateField.trim(); + this.repopulateFieldAsBoolean = "true".equalsIgnoreCase(this.repopulateFieldAsString) ? (true) : (false); + } + + public void validate(Object object) throws ValidationException { + doValidate(object); + if (repopulateFieldAsBoolean) { + repopulateField(object); + } + } + + public void repopulateField(Object object) throws ValidationException { + + ActionInvocation invocation = ActionContext.getContext().getActionInvocation(); + Map<String, Object> conversionErrors = ActionContext.getContext().getConversionErrors(); + + String fieldName = getFieldName(); + String fullFieldName = getValidatorContext().getFullFieldName(fieldName); if (conversionErrors.containsKey(fullFieldName)) { Object value = conversionErrors.get(fullFieldName); @@ -170,18 +171,18 @@ public void repopulateField(Object object) throws ValidationException { if (value instanceof String[]) { // take the first element, if possible String[] tmpValue = (String[]) value; - if (tmpValue != null && (tmpValue.length > 0)) { + if ((tmpValue.length > 0)) { doExprOverride = true; - fakeParams.put(fullFieldName, "'" + tmpValue[0] + "'"); + fakeParams.put(fullFieldName, escape(tmpValue[0])); } else { if (LOG.isWarnEnabled()) { - LOG.warn("value is an empty array of String or with first element in it as null [" + value + "], will not repopulate conversion error "); + LOG.warn("value is an empty array of String or with first element in it as null [" + value + "], will not repopulate conversion error "); } } } else if (value instanceof String) { String tmpValue = (String) value; doExprOverride = true; - fakeParams.put(fullFieldName, "'" + tmpValue + "'"); + fakeParams.put(fullFieldName, escape(tmpValue)); } else { // opps... it should be if (LOG.isWarnEnabled()) { @@ -198,7 +199,11 @@ public void beforeResult(ActionInvocation invocation, String resultCode) { }); } } - } - - protected abstract void doValidate(Object object) throws ValidationException; + } + + protected String escape(String value) { + return "\"" + StringEscapeUtils.escapeJava(value) + "\""; + } + + protected abstract void doValidate(Object object) throws ValidationException; }
xwork-core/src/test/java/com/opensymphony/xwork2/interceptor/ConversionErrorInterceptorTest.java+5 −4 modified@@ -35,7 +35,7 @@ public class ConversionErrorInterceptorTest extends XWorkTestCase { protected ActionContext context; protected ActionInvocation invocation; protected ConversionErrorInterceptor interceptor; - protected Map<String,Object> conversionErrors; + protected Map<String, Object> conversionErrors; protected Mock mockInvocation; protected ValueStack stack; @@ -86,10 +86,11 @@ public void testWithPreResultListener() throws Exception { /** * See WW-3668 + * * @throws Exception */ public void testWithPreResultListenerAgainstMaliciousCode() throws Exception { - conversionErrors.put("foo", "' + #root + '"); + conversionErrors.put("foo", "\" + #root + \""); ActionContext ac = createActionContext(); @@ -104,7 +105,7 @@ public void testWithPreResultListenerAgainstMaliciousCode() throws Exception { assertTrue(action.hasFieldErrors()); assertNotNull(action.getFieldErrors().get("foo")); - assertEquals("' + #root + '", stack.findValue("foo")); + assertEquals("\" + #root + \"", stack.findValue("foo")); } private MockActionInvocation createActionInvocation(ActionContext ac) { @@ -137,7 +138,7 @@ protected void setUp() throws Exception { invocation = (ActionInvocation) mockInvocation.proxy(); stack = ActionContext.getContext().getValueStack(); context = new ActionContext(stack.getContext()); - conversionErrors = new HashMap<String,Object>(); + conversionErrors = new HashMap<String, Object>(); context.setConversionErrors(conversionErrors); mockInvocation.matchAndReturn("getInvocationContext", context); mockInvocation.expect("addPreResultListener", C.isA(PreResultListener.class));
5f54b8d087f5WW-3668 - Vulnerability: User input is evaluated as an OGNL expression when there's a conversion error (a demonstrative patch).
1 file changed · +52 −14
xwork-core/src/test/java/com/opensymphony/xwork2/interceptor/ConversionErrorInterceptorTest.java+52 −14 modified@@ -35,13 +35,13 @@ public class ConversionErrorInterceptorTest extends XWorkTestCase { protected ActionContext context; protected ActionInvocation invocation; protected ConversionErrorInterceptor interceptor; - protected Map conversionErrors; + protected Map<String,Object> conversionErrors; protected Mock mockInvocation; protected ValueStack stack; public void testFieldErrorAdded() throws Exception { - conversionErrors.put("foo", new Long(123)); + conversionErrors.put("foo", 123L); SimpleAction action = new SimpleAction(); mockInvocation.expectAndReturn("getAction", action); @@ -69,19 +69,12 @@ public void testFieldErrorWithMapKeyAdded() throws Exception { public void testWithPreResultListener() throws Exception { conversionErrors.put("foo", "Hello"); - ActionContext ac = new ActionContext(stack.getContext()); - ac.setConversionErrors(conversionErrors); - ac.setValueStack(stack); + ActionContext ac = createActionContext(); + MockActionInvocation mai = createActionInvocation(ac); + SimpleAction action = createAction(mai); - MockActionInvocation mai = new MockActionInvocation(); - mai.setInvocationContext(ac); - mai.setStack(stack); - SimpleAction action = new SimpleAction(); - action.setFoo(55); - mai.setAction(action); - stack.push(action); assertNull(action.getFieldErrors().get("foo")); - assertEquals(new Integer(55), stack.findValue("foo")); + assertEquals(55, stack.findValue("foo")); interceptor.intercept(mai); @@ -91,6 +84,51 @@ public void testWithPreResultListener() throws Exception { assertEquals("Hello", stack.findValue("foo")); // assume that the original value is reset } + /** + * See WW-3668 + * @throws Exception + */ + public void testWithPreResultListenerAgainstMaliciousCode() throws Exception { + conversionErrors.put("foo", "' + #root + '"); + + ActionContext ac = createActionContext(); + + MockActionInvocation mai = createActionInvocation(ac); + + SimpleAction action = createAction(mai); + assertNull(action.getFieldErrors().get("foo")); + assertEquals(55, stack.findValue("foo")); + + interceptor.intercept(mai); + + assertTrue(action.hasFieldErrors()); + assertNotNull(action.getFieldErrors().get("foo")); + + assertEquals("' + #root + '", stack.findValue("foo")); + } + + private MockActionInvocation createActionInvocation(ActionContext ac) { + MockActionInvocation mai = new MockActionInvocation(); + mai.setInvocationContext(ac); + mai.setStack(stack); + return mai; + } + + private SimpleAction createAction(MockActionInvocation mai) { + SimpleAction action = new SimpleAction(); + action.setFoo(55); + mai.setAction(action); + stack.push(action); + return action; + } + + private ActionContext createActionContext() { + ActionContext ac = new ActionContext(stack.getContext()); + ac.setConversionErrors(conversionErrors); + ac.setValueStack(stack); + return ac; + } + @Override protected void setUp() throws Exception { super.setUp(); @@ -99,7 +137,7 @@ protected void setUp() throws Exception { invocation = (ActionInvocation) mockInvocation.proxy(); stack = ActionContext.getContext().getValueStack(); context = new ActionContext(stack.getContext()); - conversionErrors = new HashMap(); + conversionErrors = new HashMap<String,Object>(); context.setConversionErrors(conversionErrors); mockInvocation.matchAndReturn("getInvocationContext", context); mockInvocation.expect("addPreResultListener", C.isA(PreResultListener.class));
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
9- jvn.jp/en/jp/JVN79099262/index.htmlnvdThird Party AdvisoryVDB EntryWEB
- jvndb.jvn.jp/jvndb/JVNDB-2012-000012nvdThird Party AdvisoryVDB EntryWEB
- struts.apache.org/2.3.1.2/docs/s2-007.htmlnvdVendor AdvisoryWEB
- github.com/advisories/GHSA-mwrx-hx6x-3hhvghsaADVISORY
- issues.apache.org/jira/browse/WW-3668nvdIssue TrackingVendor AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2012-0838ghsaADVISORY
- github.com/apache/struts/commit/25e50069d60434a30395e3a98357ffba2bed427eghsaWEB
- github.com/apache/struts/commit/5f54b8d087f5125d96838aafa5f64c2190e6885bghsaWEB
- github.com/apache/struts/commit/b4265d369dc29d57a9f2846a85b26598e83f3892ghsaWEB
News mentions
0No linked articles in our index yet.