CVE-2013-2135
Description
Apache Struts 2 before 2.3.14.3 allows remote attackers to execute arbitrary OGNL code via a request with a crafted value that contains both "${}" and "%{}" sequences, which causes the OGNL code to be evaluated twice.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.apache.struts:struts2-coreMaven | >= 2.0.0, < 2.3.14.3 | 2.3.14.3 |
org.apache.struts.xwork:xwork-coreMaven | >= 2.0.0, < 2.3.14.3 | 2.3.14.3 |
Affected products
1Patches
7041206d2a693WW-4095 WW-4094 Changes how pattern is compiled to be once per instance and changes default regexp to match underscore
2 files changed · +12 −5
core/src/main/java/org/apache/struts2/dispatcher/mapper/DefaultActionMapper.java+6 −5 modified@@ -38,6 +38,7 @@ import javax.servlet.http.HttpServletRequest; import java.util.*; +import java.util.regex.Pattern; /** * <!-- START SNIPPET: javadoc --> @@ -170,7 +171,7 @@ public class DefaultActionMapper implements ActionMapper { protected boolean allowSlashesInActionNames = false; protected boolean alwaysSelectFullNamespace = false; protected PrefixTrie prefixTrie = null; - protected String allowedActionNames = "[a-z]*[A-Z]*[0-9]*[.\\-_!/]*"; + protected Pattern allowedActionNames = Pattern.compile("[a-zA-Z0-9._!/\\-]*"); protected List<String> extensions = new ArrayList<String>() {{ add("action"); @@ -262,7 +263,7 @@ public void setAlwaysSelectFullNamespace(String val) { @Inject(value = StrutsConstants.STRUTS_ALLOWED_ACTION_NAMES, required = false) public void setAllowedActionNames(String allowedActionNames) { - this.allowedActionNames = allowedActionNames; + this.allowedActionNames = Pattern.compile(allowedActionNames); } @Inject @@ -432,15 +433,15 @@ protected void parseNameAndNamespace(String uri, ActionMapping mapping, Configur * @return safe action name */ protected String cleanupActionName(final String rawActionName) { - if (rawActionName.matches(allowedActionNames)) { + if (allowedActionNames.matcher(rawActionName).matches()) { return rawActionName; } else { if (LOG.isWarnEnabled()) { - LOG.warn("Action [#0] do not match allowed action names pattern [#1], cleaning it up!", + LOG.warn("Action [#0] does not match allowed action names pattern [#1], cleaning it up!", rawActionName, allowedActionNames); } String cleanActionName = rawActionName; - for(String chunk : rawActionName.split(allowedActionNames)) { + for(String chunk : allowedActionNames.split(rawActionName)) { cleanActionName = cleanActionName.replace(chunk, ""); } if (LOG.isDebugEnabled()) {
core/src/test/java/org/apache/struts2/dispatcher/mapper/DefaultActionMapperTest.java+6 −0 modified@@ -764,6 +764,12 @@ public void testAllowedActionNames() throws Exception { actionName = "test-action"; assertEquals("test-action", mapper.cleanupActionName(actionName)); + + actionName = "test_action"; + assertEquals("test_action", mapper.cleanupActionName(actionName)); + + actionName = "test!bar.action"; + assertEquals("test!bar.action", mapper.cleanupActionName(actionName)); } }
711cf0201cddMerged from STRUTS_2_3_14_2_X
5 files changed · +83 −11
core/src/main/java/org/apache/struts2/dispatcher/mapper/DefaultActionMapper.java+37 −7 modified@@ -27,6 +27,8 @@ import com.opensymphony.xwork2.config.entities.PackageConfig; import com.opensymphony.xwork2.inject.Container; import com.opensymphony.xwork2.inject.Inject; +import com.opensymphony.xwork2.util.logging.Logger; +import com.opensymphony.xwork2.util.logging.LoggerFactory; import org.apache.commons.lang3.StringUtils; import org.apache.struts2.RequestUtils; import org.apache.struts2.ServletActionContext; @@ -35,12 +37,7 @@ import org.apache.struts2.util.PrefixTrie; import javax.servlet.http.HttpServletRequest; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; /** * <!-- START SNIPPET: javadoc --> @@ -162,6 +159,8 @@ */ public class DefaultActionMapper implements ActionMapper { + private static final Logger LOG = LoggerFactory.getLogger(DefaultActionMapper.class); + protected static final String METHOD_PREFIX = "method:"; protected static final String ACTION_PREFIX = "action:"; protected static final String REDIRECT_PREFIX = "redirect:"; @@ -171,6 +170,7 @@ public class DefaultActionMapper implements ActionMapper { protected boolean allowSlashesInActionNames = false; protected boolean alwaysSelectFullNamespace = false; protected PrefixTrie prefixTrie = null; + protected String allowedActionNames = "[a-z]*[A-Z]*[0-9]*[.\\-_!/]*"; protected List<String> extensions = new ArrayList<String>() {{ add("action"); @@ -260,6 +260,11 @@ public void setAlwaysSelectFullNamespace(String val) { this.alwaysSelectFullNamespace = "true".equals(val); } + @Inject(value = StrutsConstants.STRUTS_ALLOWED_ACTION_NAMES, required = false) + public void setAllowedActionNames(String allowedActionNames) { + this.allowedActionNames = allowedActionNames; + } + @Inject public void setContainer(Container container) { this.container = container; @@ -417,7 +422,32 @@ protected void parseNameAndNamespace(String uri, ActionMapping mapping, Configur } mapping.setNamespace(namespace); - mapping.setName(name); + mapping.setName(cleanupActionName(name)); + } + + /** + * Cleans up action name from suspicious characters + * + * @param rawActionName action name extracted from URI + * @return safe action name + */ + protected String cleanupActionName(final String rawActionName) { + if (rawActionName.matches(allowedActionNames)) { + return rawActionName; + } else { + if (LOG.isWarnEnabled()) { + LOG.warn("Action [#0] do not match allowed action names pattern [#1], cleaning it up!", + rawActionName, allowedActionNames); + } + String cleanActionName = rawActionName; + for(String chunk : rawActionName.split(allowedActionNames)) { + cleanActionName = cleanActionName.replace(chunk, ""); + } + if (LOG.isDebugEnabled()) { + LOG.debug("Cleaned action name [#0]", cleanActionName); + } + return cleanActionName; + } } /**
core/src/main/java/org/apache/struts2/StrutsConstants.java+3 −0 modified@@ -252,4 +252,7 @@ public final class StrutsConstants { public static final String STRUTS_EXPRESSION_PARSER = "struts.expression.parser"; + /** actions names' whitelist **/ + public static final String STRUTS_ALLOWED_ACTION_NAMES = "struts.allowed.action.names"; + }
core/src/test/java/org/apache/struts2/dispatcher/mapper/DefaultActionMapperTest.java+19 −0 modified@@ -747,4 +747,23 @@ public void testSetExtension() throws Exception { } + public void testAllowedActionNames() throws Exception { + DefaultActionMapper mapper = new DefaultActionMapper(); + + String actionName = "action"; + assertEquals(actionName, mapper.cleanupActionName(actionName)); + + actionName = "${action}"; + assertEquals("action", mapper.cleanupActionName(actionName)); + + actionName = "${${%{action}}}"; + assertEquals("action", mapper.cleanupActionName(actionName)); + + actionName = "${#foo='action',#foo}"; + assertEquals("fooactionfoo", mapper.cleanupActionName(actionName)); + + actionName = "test-action"; + assertEquals("test-action", mapper.cleanupActionName(actionName)); + } + }
xwork-core/src/main/java/com/opensymphony/xwork2/util/OgnlTextParser.java+2 −3 modified@@ -11,17 +11,16 @@ public Object evaluate(char[] openChars, String expression, TextParseUtil.Parsed // deal with the "pure" expressions first! //expression = expression.trim(); Object result = expression; + int pos = 0; + for (char open : openChars) { int loopCount = 1; - int pos = 0; - //this creates an implicit StringBuffer and shouldn't be used in the inner loop final String lookupChars = open + "{"; while (true) { int start = expression.indexOf(lookupChars, pos); if (start == -1) { - pos = 0; loopCount++; start = expression.indexOf(lookupChars); }
xwork-core/src/test/java/com/opensymphony/xwork2/util/TextParseUtilTest.java+22 −1 modified@@ -97,6 +97,24 @@ public void testTranslateVariables() { assertEquals("count must be between 123 and 456, current value is 98765.", s); } + public void testNestedExpression() throws Exception { + ValueStack stack = ActionContext.getContext().getValueStack(); + stack.push(new HashMap<String, Object>() {{ put("foo", "${%{1+1}}"); }}); + String s = TextParseUtil.translateVariables("${foo}", stack); + assertEquals("${%{1+1}}", s); + stack.pop(); + } + + public void testMixedOpenChars() throws Exception { + ValueStack stack = ActionContext.getContext().getValueStack(); + stack.push(new HashMap<String, Object>() {{ put("foo", "bar"); }}); + String s = TextParseUtil.translateVariables("${foo}-%{foo}", stack); + assertEquals("bar-bar", s); + s = TextParseUtil.translateVariables("%{foo}-${foo}", stack); + assertEquals("%{foo}-bar", s); // this is bad, but it is the only way not to double evaluate passed expression + stack.pop(); + } + public void testCommaDelimitedStringToSet() { assertEquals(0, TextParseUtil.commaDelimitedStringToSet("").size()); assertEquals(new HashSet<String>(Arrays.asList("foo", "bar", "tee")), @@ -132,10 +150,13 @@ public void testTranslateVariablesNoRecursive() { public void testTranslateVariablesRecursive() { ValueStack stack = ActionContext.getContext().getValueStack(); - stack.push(new HashMap<String, Object>() {{ put("foo", "${1+1}"); }}); + stack.push(new HashMap<String, Object>() {{ put("foo", "${1+1}"); put("bar", "${${1+2}}"); }}); Object s = TextParseUtil.translateVariables('$', "foo: ${foo}", stack, String.class, null, 2); assertEquals("foo: 2", s); + + s = TextParseUtil.translateVariables('$', "foo: ${bar}", stack, String.class, null, 1); + assertEquals("foo: ${${1+2}}", s); } public void testTranslateVariablesWithNull() {
cfb6e9afbae3WW-4090 Uses warn level instead of debug
1 file changed · +2 −2
core/src/main/java/org/apache/struts2/dispatcher/mapper/DefaultActionMapper.java+2 −2 modified@@ -435,8 +435,8 @@ protected String cleanupActionName(final String rawActionName) { if (rawActionName.matches(allowedActionNames)) { return rawActionName; } else { - if (LOG.isDebugEnabled()) { - LOG.debug("Action [#0] do not match allowed action names pattern [#1], cleaning it up!", + if (LOG.isWarnEnabled()) { + LOG.warn("Action [#0] do not match allowed action names pattern [#1], cleaning it up!", rawActionName, allowedActionNames); } String cleanActionName = rawActionName;
8b4fc81daeeaWW-4090 Add some logging
1 file changed · +11 −0
core/src/main/java/org/apache/struts2/dispatcher/mapper/DefaultActionMapper.java+11 −0 modified@@ -27,6 +27,8 @@ import com.opensymphony.xwork2.config.entities.PackageConfig; import com.opensymphony.xwork2.inject.Container; import com.opensymphony.xwork2.inject.Inject; +import com.opensymphony.xwork2.util.logging.Logger; +import com.opensymphony.xwork2.util.logging.LoggerFactory; import org.apache.commons.lang3.StringUtils; import org.apache.struts2.RequestUtils; import org.apache.struts2.ServletActionContext; @@ -157,6 +159,8 @@ */ public class DefaultActionMapper implements ActionMapper { + private static final Logger LOG = LoggerFactory.getLogger(DefaultActionMapper.class); + protected static final String METHOD_PREFIX = "method:"; protected static final String ACTION_PREFIX = "action:"; protected static final String REDIRECT_PREFIX = "redirect:"; @@ -431,10 +435,17 @@ protected String cleanupActionName(final String rawActionName) { if (rawActionName.matches(allowedActionNames)) { return rawActionName; } else { + if (LOG.isDebugEnabled()) { + LOG.debug("Action [#0] do not match allowed action names pattern [#1], cleaning it up!", + rawActionName, allowedActionNames); + } String cleanActionName = rawActionName; for(String chunk : rawActionName.split(allowedActionNames)) { cleanActionName = cleanActionName.replace(chunk, ""); } + if (LOG.isDebugEnabled()) { + LOG.debug("Cleaned action name [#0]", cleanActionName); + } return cleanActionName; } }
54e5c912ebd9WW-4090 Removes double evaluation of parsed expression
2 files changed · +24 −4
xwork-core/src/main/java/com/opensymphony/xwork2/util/OgnlTextParser.java+2 −3 modified@@ -11,17 +11,16 @@ public Object evaluate(char[] openChars, String expression, TextParseUtil.Parsed // deal with the "pure" expressions first! //expression = expression.trim(); Object result = expression; + int pos = 0; + for (char open : openChars) { int loopCount = 1; - int pos = 0; - //this creates an implicit StringBuffer and shouldn't be used in the inner loop final String lookupChars = open + "{"; while (true) { int start = expression.indexOf(lookupChars, pos); if (start == -1) { - pos = 0; loopCount++; start = expression.indexOf(lookupChars); }
xwork-core/src/test/java/com/opensymphony/xwork2/util/TextParseUtilTest.java+22 −1 modified@@ -97,6 +97,24 @@ public void testTranslateVariables() { assertEquals("count must be between 123 and 456, current value is 98765.", s); } + public void testNestedExpression() throws Exception { + ValueStack stack = ActionContext.getContext().getValueStack(); + stack.push(new HashMap<String, Object>() {{ put("foo", "${%{1+1}}"); }}); + String s = TextParseUtil.translateVariables("${foo}", stack); + assertEquals("${%{1+1}}", s); + stack.pop(); + } + + public void testMixedOpenChars() throws Exception { + ValueStack stack = ActionContext.getContext().getValueStack(); + stack.push(new HashMap<String, Object>() {{ put("foo", "bar"); }}); + String s = TextParseUtil.translateVariables("${foo}-%{foo}", stack); + assertEquals("bar-bar", s); + s = TextParseUtil.translateVariables("%{foo}-${foo}", stack); + assertEquals("%{foo}-bar", s); // this is bad, but it is the only way not to double evaluate passed expression + stack.pop(); + } + public void testCommaDelimitedStringToSet() { assertEquals(0, TextParseUtil.commaDelimitedStringToSet("").size()); assertEquals(new HashSet<String>(Arrays.asList("foo", "bar", "tee")), @@ -132,10 +150,13 @@ public void testTranslateVariablesNoRecursive() { public void testTranslateVariablesRecursive() { ValueStack stack = ActionContext.getContext().getValueStack(); - stack.push(new HashMap<String, Object>() {{ put("foo", "${1+1}"); }}); + stack.push(new HashMap<String, Object>() {{ put("foo", "${1+1}"); put("bar", "${${1+2}}"); }}); Object s = TextParseUtil.translateVariables('$', "foo: ${foo}", stack, String.class, null, 2); assertEquals("foo: 2", s); + + s = TextParseUtil.translateVariables('$', "foo: ${bar}", stack, String.class, null, 1); + assertEquals("foo: ${${1+2}}", s); } public void testTranslateVariablesWithNull() {
01e6b251b4dbWW-4090 Itroduces actions names' whitelisting
3 files changed · +48 −7
core/src/main/java/org/apache/struts2/dispatcher/mapper/DefaultActionMapper.java+26 −7 modified@@ -35,12 +35,7 @@ import org.apache.struts2.util.PrefixTrie; import javax.servlet.http.HttpServletRequest; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; /** * <!-- START SNIPPET: javadoc --> @@ -171,6 +166,7 @@ public class DefaultActionMapper implements ActionMapper { protected boolean allowSlashesInActionNames = false; protected boolean alwaysSelectFullNamespace = false; protected PrefixTrie prefixTrie = null; + protected String allowedActionNames = "[a-z]*[A-Z]*[0-9]*[.\\-_!/]*"; protected List<String> extensions = new ArrayList<String>() {{ add("action"); @@ -260,6 +256,11 @@ public void setAlwaysSelectFullNamespace(String val) { this.alwaysSelectFullNamespace = "true".equals(val); } + @Inject(value = StrutsConstants.STRUTS_ALLOWED_ACTION_NAMES, required = false) + public void setAllowedActionNames(String allowedActionNames) { + this.allowedActionNames = allowedActionNames; + } + @Inject public void setContainer(Container container) { this.container = container; @@ -417,7 +418,25 @@ protected void parseNameAndNamespace(String uri, ActionMapping mapping, Configur } mapping.setNamespace(namespace); - mapping.setName(name); + mapping.setName(cleanupActionName(name)); + } + + /** + * Cleans up action name from suspicious characters + * + * @param rawActionName action name extracted from URI + * @return safe action name + */ + protected String cleanupActionName(final String rawActionName) { + if (rawActionName.matches(allowedActionNames)) { + return rawActionName; + } else { + String cleanActionName = rawActionName; + for(String chunk : rawActionName.split(allowedActionNames)) { + cleanActionName = cleanActionName.replace(chunk, ""); + } + return cleanActionName; + } } /**
core/src/main/java/org/apache/struts2/StrutsConstants.java+3 −0 modified@@ -252,4 +252,7 @@ public final class StrutsConstants { public static final String STRUTS_EXPRESSION_PARSER = "struts.expression.parser"; + /** actions names' whitelist **/ + public static final String STRUTS_ALLOWED_ACTION_NAMES = "struts.allowed.action.names"; + }
core/src/test/java/org/apache/struts2/dispatcher/mapper/DefaultActionMapperTest.java+19 −0 modified@@ -747,4 +747,23 @@ public void testSetExtension() throws Exception { } + public void testAllowedActionNames() throws Exception { + DefaultActionMapper mapper = new DefaultActionMapper(); + + String actionName = "action"; + assertEquals(actionName, mapper.cleanupActionName(actionName)); + + actionName = "${action}"; + assertEquals("action", mapper.cleanupActionName(actionName)); + + actionName = "${${%{action}}}"; + assertEquals("action", mapper.cleanupActionName(actionName)); + + actionName = "${#foo='action',#foo}"; + assertEquals("fooactionfoo", mapper.cleanupActionName(actionName)); + + actionName = "test-action"; + assertEquals("test-action", mapper.cleanupActionName(actionName)); + } + }
113c47082c09WW-4090 Updates version of archetypes to match new version
1 file changed · +6 −6
src/site/resources/archetype-catalog.xml+6 −6 modified@@ -7,42 +7,42 @@ <archetype> <groupId>org.apache.struts</groupId> <artifactId>struts2-archetype-blank</artifactId> - <version>2.3.14.2</version> + <version>2.3.14.3</version> <repository>http://repo1.maven.org/maven2/</repository> <description>Struts 2 Archetypes - Blank</description> </archetype> <archetype> <groupId>org.apache.struts</groupId> <artifactId>struts2-archetype-convention</artifactId> - <version>2.3.14.2</version> + <version>2.3.14.3</version> <repository>http://repo1.maven.org/maven2/</repository> <description>Struts 2 Archetypes - Blank Convention</description> </archetype> <archetype> <groupId>org.apache.struts</groupId> <artifactId>struts2-archetype-dbportlet</artifactId> - <version>2.3.14.2</version> + <version>2.3.14.3</version> <repository>http://repo1.maven.org/maven2/</repository> <description>Struts 2 Archetypes - Database Portlet</description> </archetype> <archetype> <groupId>org.apache.struts</groupId> <artifactId>struts2-archetype-plugin</artifactId> - <version>2.3.14.2</version> + <version>2.3.14.3</version> <repository>http://repo1.maven.org/maven2/</repository> <description>Struts 2 Archetypes - Plugin</description> </archetype> <archetype> <groupId>org.apache.struts</groupId> <artifactId>struts2-archetype-portlet</artifactId> - <version>2.3.14.2</version> + <version>2.3.14.3</version> <repository>http://repo1.maven.org/maven2/</repository> <description>Struts 2 Archetypes - Portlet</description> </archetype> <archetype> <groupId>org.apache.struts</groupId> <artifactId>struts2-archetype-starter</artifactId> - <version>2.3.14.2</version> + <version>2.3.14.3</version> <repository>http://repo1.maven.org/maven2/</repository> <description>Struts 2 Archetypes - Starter</description> </archetype>
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
18- struts.apache.org/development/2.x/docs/s2-015.htmlnvdVendor AdvisoryWEB
- www.oracle.com/technetwork/topics/security/cpujan2014-1972949.htmlnvdThird Party AdvisoryWEB
- www.oracle.com/technetwork/topics/security/cpuoct2013-1899837.htmlnvdThird Party AdvisoryWEB
- www.securityfocus.com/bid/64758nvdThird Party AdvisoryVDB Entry
- cwiki.apache.org/confluence/display/WW/S2-015nvdVendor AdvisoryWEB
- github.com/advisories/GHSA-pw8r-x2qm-3h5mghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2013-2135ghsaADVISORY
- github.com/apache/struts/commit/01e6b251b4db78bfb7971033652e81d1af4cb3e0ghsaWEB
- github.com/apache/struts/commit/041206d2a693d02c0cb2e72765275e55ba14049fghsaWEB
- github.com/apache/struts/commit/113c47082c09818bcef65acc436a2d0c7c47aa6cghsaWEB
- github.com/apache/struts/commit/54e5c912ebd9a1599bfcf7a719da17c28127bbe3ghsaWEB
- github.com/apache/struts/commit/711cf0201cdd319a38cf29238913312355db29baghsaWEB
- github.com/apache/struts/commit/8b4fc81daeea3834bcbf73de5f48d0021917aa37ghsaWEB
- github.com/apache/struts/commit/cfb6e9afbae320a4dd5bdd655154ab9fe5a92c16ghsaWEB
- issues.apache.org/jira/browse/WW-4090ghsaWEB
- issues.apache.org/jira/browse/WW-4094ghsaWEB
- issues.apache.org/jira/browse/WW-4095ghsaWEB
- web.archive.org/web/20140410223942/http://www.securityfocus.com/bid/64758ghsaWEB
News mentions
0No linked articles in our index yet.