This is an automated email from the ASF dual-hosted git repository.

lukaszlenart pushed a commit to branch WW-5706-restful-action-name-validation-6x
in repository https://gitbox.apache.org/repos/asf/struts.git

commit 222a5e4b3912f2bbf5dee9eca18dc703df6d2fcf
Author: Lukasz Lenart <[email protected]>
AuthorDate: Sun Aug 30 08:41:01 2026 +0200

    WW-5706 fix(core): align RestfulActionMapper action name handling with 
DefaultActionMapper
    
    RestfulActionMapper derived the action name straight from the request
    URI, unlike DefaultActionMapper which validates it via cleanupActionName
    against the allowedActionNames pattern. Apply the same check (and the
    struts.allowed.action.names / struts.default.action.name settings) so
    both mappers handle action names consistently.
    
    Fixes: https://issues.apache.org/jira/browse/WW-5706
    
    Co-Authored-By: Claude Opus 4.8 <[email protected]>
    Claude-Session: https://claude.ai/code/session_016XMyQ1CRuYZkqygmD4aGHv
---
 .../dispatcher/mapper/RestfulActionMapper.java     | 40 +++++++++++++++++++++-
 .../dispatcher/mapper/RestfulActionMapperTest.java | 16 +++++++++
 2 files changed, 55 insertions(+), 1 deletion(-)

diff --git 
a/core/src/main/java/org/apache/struts2/dispatcher/mapper/RestfulActionMapper.java
 
b/core/src/main/java/org/apache/struts2/dispatcher/mapper/RestfulActionMapper.java
index 1709834a1..56b111aa3 100644
--- 
a/core/src/main/java/org/apache/struts2/dispatcher/mapper/RestfulActionMapper.java
+++ 
b/core/src/main/java/org/apache/struts2/dispatcher/mapper/RestfulActionMapper.java
@@ -23,12 +23,14 @@ import com.opensymphony.xwork2.inject.Inject;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.apache.struts2.RequestUtils;
+import org.apache.struts2.StrutsConstants;
 import org.apache.struts2.url.UrlDecoder;
 
 import javax.servlet.http.HttpServletRequest;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.StringTokenizer;
+import java.util.regex.Pattern;
 
 /**
  * Simple Restfull Action Mapper to support REST application
@@ -41,11 +43,31 @@ public class RestfulActionMapper implements ActionMapper {
 
     private UrlDecoder decoder;
 
+    /**
+     * Matches action names allowed in the request URI, aligned with {@link 
DefaultActionMapper}.
+     */
+    private Pattern allowedActionNames = 
Pattern.compile("[a-zA-Z0-9._!/\\-]*");
+
+    /**
+     * Action name used when the name extracted from the URI is not allowed, 
aligned with {@link DefaultActionMapper}.
+     */
+    private String defaultActionName = "index";
+
     @Inject
     public void setDecoder(UrlDecoder decoder) {
         this.decoder = decoder;
     }
 
+    @Inject(value = StrutsConstants.STRUTS_ALLOWED_ACTION_NAMES, required = 
false)
+    public void setAllowedActionNames(String allowedActionNames) {
+        this.allowedActionNames = Pattern.compile(allowedActionNames);
+    }
+
+    @Inject(value = StrutsConstants.STRUTS_DEFAULT_ACTION_NAME, required = 
false)
+    public void setDefaultActionName(String defaultActionName) {
+        this.defaultActionName = defaultActionName;
+    }
+
     /* (non-Javadoc)
      * @see 
org.apache.struts2.dispatcher.mapper.ActionMapper#getMapping(javax.servlet.http.HttpServletRequest)
      */
@@ -57,7 +79,7 @@ public class RestfulActionMapper implements ActionMapper {
             return null;
         }
 
-        String actionName = uri.substring(1, nextSlash);
+        String actionName = cleanupActionName(uri.substring(1, nextSlash));
         Map<String, Object> parameters = new HashMap<>();
         try {
             StringTokenizer st = new StringTokenizer(uri.substring(nextSlash), 
"/");
@@ -96,6 +118,22 @@ public class RestfulActionMapper implements ActionMapper {
         return new ActionMapping(actionName, null, null, null);
     }
 
+    /**
+     * Checks action name against the allowed pattern; if it does not match, 
returns the default action name.
+     * Mirrors {@link DefaultActionMapper#cleanupActionName(String)}.
+     *
+     * @param rawActionName action name extracted from the URI
+     * @return safe action name
+     */
+    protected String cleanupActionName(final String rawActionName) {
+        if (allowedActionNames.matcher(rawActionName).matches()) {
+            return rawActionName;
+        } else {
+            LOG.warn("{} did not match allowed action names {} - default 
action {} will be used!", rawActionName, allowedActionNames, defaultActionName);
+            return defaultActionName;
+        }
+    }
+
     /* (non-Javadoc)
      * @see 
org.apache.struts2.dispatcher.mapper.ActionMapper#getUriFromActionMapping(org.apache.struts2.dispatcher.mapper.ActionMapping)
      */
diff --git 
a/core/src/test/java/org/apache/struts2/dispatcher/mapper/RestfulActionMapperTest.java
 
b/core/src/test/java/org/apache/struts2/dispatcher/mapper/RestfulActionMapperTest.java
index 0ee3b1956..ecc284cd0 100644
--- 
a/core/src/test/java/org/apache/struts2/dispatcher/mapper/RestfulActionMapperTest.java
+++ 
b/core/src/test/java/org/apache/struts2/dispatcher/mapper/RestfulActionMapperTest.java
@@ -105,6 +105,22 @@ public class RestfulActionMapperTest extends 
StrutsInternalTestCase {
         assertEquals("europe", am.getParams().get("region"));
     }
 
+    public void testGetMappingRejectsActionNameWithDisallowedCharacters() {
+        StrutsMockHttpServletRequest request = new 
StrutsMockHttpServletRequest();
+        request.setupGetServletPath("/%{1+1}/x");
+
+        ActionMapping am = mapper.getMapping(request, null);
+        assertEquals("index", am.getName());
+    }
+
+    public void testGetMappingAcceptsRegularActionName() {
+        StrutsMockHttpServletRequest request = new 
StrutsMockHttpServletRequest();
+        request.setupGetServletPath("/my-app.action/x");
+
+        ActionMapping am = mapper.getMapping(request, null);
+        assertEquals("my-app.action", am.getName());
+    }
+
     protected void setUp() throws Exception {
         super.setUp();
         mapper = new RestfulActionMapper();

Reply via email to