VYPR
High severityNVD Advisory· Published Jul 16, 2013· Updated Apr 29, 2026

CVE-2013-2135

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.

PackageAffected versionsPatched versions
org.apache.struts:struts2-coreMaven
>= 2.0.0, < 2.3.14.32.3.14.3
org.apache.struts.xwork:xwork-coreMaven
>= 2.0.0, < 2.3.14.32.3.14.3

Affected products

1
  • cpe:2.3:a:apache:struts:*:*:*:*:*:*:*:*
    Range: >=2.0.0,<2.3.14.3

Patches

7
041206d2a693

WW-4095 WW-4094 Changes how pattern is compiled to be once per instance and changes default regexp to match underscore

https://github.com/apache/strutsLukasz LenartJun 6, 2013via ghsa
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));
         }
     
     }
    
711cf0201cdd

Merged from STRUTS_2_3_14_2_X

https://github.com/apache/strutsLukasz LenartJun 6, 2013via ghsa
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() {
    
cfb6e9afbae3

WW-4090 Uses warn level instead of debug

https://github.com/apache/strutsLukasz LenartJun 3, 2013via ghsa
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;
    
8b4fc81daeea

WW-4090 Add some logging

https://github.com/apache/strutsLukasz LenartJun 3, 2013via ghsa
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;
             }
         }
    
54e5c912ebd9

WW-4090 Removes double evaluation of parsed expression

https://github.com/apache/strutsLukasz LenartJun 3, 2013via ghsa
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() {
    
01e6b251b4db

WW-4090 Itroduces actions names' whitelisting

https://github.com/apache/strutsLukasz LenartJun 3, 2013via ghsa
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));
    +    }
    +
     }
    
113c47082c09

WW-4090 Updates version of archetypes to match new version

https://github.com/apache/strutsLukasz LenartJun 3, 2013via ghsa
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

News mentions

0

No linked articles in our index yet.