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

lukaszlenart pushed a commit to branch support/struts-6-x-x
in repository https://gitbox.apache.org/repos/asf/struts.git


The following commit(s) were added to refs/heads/support/struts-6-x-x by this 
push:
     new 60ec4778d WW-5846 fix(core)!: match exemptedPaths against the request 
path (#2125)
60ec4778d is described below

commit 60ec4778d88f6625409be325f80e50c4e13fa68e
Author: Lukasz Lenart <[email protected]>
AuthorDate: Sat Nov 7 19:14:48 2026 +0100

    WW-5846 fix(core)!: match exemptedPaths against the request path (#2125)
    
    FetchMetadataInterceptor, CoopInterceptor and CoepInterceptor compared each
    exemptedPaths entry with the context path, which is the same for every
    request: an entry naming a path never matched, and an entry equal to the
    context path exempted the whole application.
    
    They now compare against RequestUtils.getUri(request), the value the action
    mapper uses: servlet path plus path info, or the included servlet path 
during
    an include dispatch. Matching is exact, so an entry includes the action
    extension, e.g. /saml/acs.action. The Fetch Metadata rejection log names the
    request path, which is the value to add to exemptedPaths.
    
    Tests now set the servlet path instead of faking a context path, and the
    struts-testing.xml fixtures use the paths a real request carries.
    
    BREAKING CHANGE: an exemptedPaths entry equal to the context path no longer
    disables the interceptor for the whole application; use the interceptor's
    "disabled" parameter instead. Entries naming a request path now take effect.
    
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 .../struts2/interceptor/CoepInterceptor.java       |  3 +-
 .../struts2/interceptor/CoopInterceptor.java       |  3 +-
 .../interceptor/FetchMetadataInterceptor.java      |  7 +--
 .../struts2/interceptor/CoepInterceptorTest.java   | 24 ++++++++++-
 .../struts2/interceptor/CoopInterceptorTest.java   | 26 +++++++++--
 .../interceptor/FetchMetadataInterceptorTest.java  | 50 ++++++++++++++++------
 core/src/test/resources/struts-testing.xml         |  6 +--
 7 files changed, 94 insertions(+), 25 deletions(-)

diff --git 
a/core/src/main/java/org/apache/struts2/interceptor/CoepInterceptor.java 
b/core/src/main/java/org/apache/struts2/interceptor/CoepInterceptor.java
index 850556e32..cafe00060 100644
--- a/core/src/main/java/org/apache/struts2/interceptor/CoepInterceptor.java
+++ b/core/src/main/java/org/apache/struts2/interceptor/CoepInterceptor.java
@@ -22,6 +22,7 @@ import com.opensymphony.xwork2.ActionInvocation;
 import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
 import com.opensymphony.xwork2.interceptor.PreResultListener;
 import com.opensymphony.xwork2.util.TextParseUtil;
+import org.apache.struts2.RequestUtils;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
@@ -58,7 +59,7 @@ public class CoepInterceptor extends AbstractInterceptor 
implements PreResultLis
     @Override
     public void beforeResult(ActionInvocation invocation, String resultCode) {
         HttpServletRequest req = 
invocation.getInvocationContext().getServletRequest();
-        final String path = req.getContextPath();
+        final String path = RequestUtils.getUri(req);
 
         if (exemptedPaths.contains(path)) {
             // no need to add headers
diff --git 
a/core/src/main/java/org/apache/struts2/interceptor/CoopInterceptor.java 
b/core/src/main/java/org/apache/struts2/interceptor/CoopInterceptor.java
index 123170baf..209db33e2 100644
--- a/core/src/main/java/org/apache/struts2/interceptor/CoopInterceptor.java
+++ b/core/src/main/java/org/apache/struts2/interceptor/CoopInterceptor.java
@@ -22,6 +22,7 @@ import com.opensymphony.xwork2.ActionInvocation;
 import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
 import com.opensymphony.xwork2.interceptor.PreResultListener;
 import com.opensymphony.xwork2.util.TextParseUtil;
+import org.apache.struts2.RequestUtils;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
@@ -60,7 +61,7 @@ public class CoopInterceptor extends AbstractInterceptor 
implements PreResultLis
     @Override
     public void beforeResult(ActionInvocation invocation, String resultCode) {
         HttpServletRequest request = 
invocation.getInvocationContext().getServletRequest();
-        String path = request.getContextPath();
+        String path = RequestUtils.getUri(request);
 
         if (isExempted(path)) {
             // no need to add headers
diff --git 
a/core/src/main/java/org/apache/struts2/interceptor/FetchMetadataInterceptor.java
 
b/core/src/main/java/org/apache/struts2/interceptor/FetchMetadataInterceptor.java
index 0f46206f2..17d7cbbda 100644
--- 
a/core/src/main/java/org/apache/struts2/interceptor/FetchMetadataInterceptor.java
+++ 
b/core/src/main/java/org/apache/struts2/interceptor/FetchMetadataInterceptor.java
@@ -23,6 +23,7 @@ import com.opensymphony.xwork2.ActionInvocation;
 import com.opensymphony.xwork2.inject.Inject;
 import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
 import com.opensymphony.xwork2.util.TextParseUtil;
+import org.apache.struts2.RequestUtils;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
@@ -67,9 +68,9 @@ public class FetchMetadataInterceptor extends 
AbstractInterceptor {
 
         addVaryHeaders(invocation);
 
-        String contextPath = request.getContextPath();
+        String path = RequestUtils.getUri(request);
         // Apply exemptions: paths/endpoints meant to be served cross-origin
-        if (exemptedPaths.contains(contextPath)) {
+        if (exemptedPaths.contains(path)) {
             return invocation.invoke();
         }
 
@@ -78,7 +79,7 @@ public class FetchMetadataInterceptor extends 
AbstractInterceptor {
             return invocation.invoke();
         }
 
-        LOG.warn("Fetch metadata rejected cross-origin request to: {}", 
contextPath);
+        LOG.warn("Fetch metadata rejected cross-origin request to: {}", path);
         return SC_FORBIDDEN;
     }
 
diff --git 
a/core/src/test/java/org/apache/struts2/interceptor/CoepInterceptorTest.java 
b/core/src/test/java/org/apache/struts2/interceptor/CoepInterceptorTest.java
index 72ea1a024..83392d6bf 100644
--- a/core/src/test/java/org/apache/struts2/interceptor/CoepInterceptorTest.java
+++ b/core/src/test/java/org/apache/struts2/interceptor/CoepInterceptorTest.java
@@ -52,7 +52,19 @@ public class CoepInterceptorTest extends 
StrutsInternalTestCase {
     }
 
     public void testExemptedPath() throws Exception{
-        request.setContextPath("/foo");
+        request.setServletPath("/foo");
+        interceptor.setEnforcingMode("true");
+
+        interceptor.intercept(mai);
+
+        String header = response.getHeader(COEP_ENFORCING_HEADER);
+        assertTrue("COEP applied to exempted path", Strings.isEmpty(header));
+    }
+
+    public void testExemptedPathInfo() throws Exception {
+        interceptor.setExemptedPaths("/rest/orders");
+        request.setServletPath("/rest");
+        request.setPathInfo("/orders");
         interceptor.setEnforcingMode("true");
 
         interceptor.intercept(mai);
@@ -61,6 +73,16 @@ public class CoepInterceptorTest extends 
StrutsInternalTestCase {
         assertTrue("COEP applied to exempted path", Strings.isEmpty(header));
     }
 
+    public void testHeaderSetWhenOnlyContextPathIsExempted() throws Exception {
+        request.setContextPath("/foo");
+        request.setServletPath("/index.action");
+        interceptor.setEnforcingMode("true");
+
+        interceptor.intercept(mai);
+
+        assertEquals("COEP header value is incorrect", HEADER_CONTENT, 
response.getHeader(COEP_ENFORCING_HEADER));
+    }
+
     public void testReportingHeader() throws Exception {
         interceptor.setEnforcingMode("false");
 
diff --git 
a/core/src/test/java/org/apache/struts2/interceptor/CoopInterceptorTest.java 
b/core/src/test/java/org/apache/struts2/interceptor/CoopInterceptorTest.java
index 544e7b5d8..b9e7aac0c 100644
--- a/core/src/test/java/org/apache/struts2/interceptor/CoopInterceptorTest.java
+++ b/core/src/test/java/org/apache/struts2/interceptor/CoopInterceptorTest.java
@@ -38,7 +38,7 @@ public class CoopInterceptorTest extends 
StrutsInternalTestCase {
     String COOP_HEADER = "Cross-Origin-Opener-Policy";
 
     public void testHeaderIsSetNonExemptedPath() throws Exception {
-        request.setContextPath("/some");
+        request.setServletPath("/some.action");
         interceptor.intercept(mai);
 
         String header = response.getHeader(COOP_HEADER);
@@ -47,16 +47,34 @@ public class CoopInterceptorTest extends 
StrutsInternalTestCase {
     }
 
     public void testHeaderIsNotSetExemptedPath() throws Exception {
-        request.setContextPath("/foo");
+        request.setServletPath("/foo");
+        interceptor.intercept(mai);
+
+        String header = response.getHeader(COOP_HEADER);
+        assertTrue("Coop header exists in exempted path", 
Strings.isEmpty(header));
+    }
+
+    public void testHeaderIsNotSetExemptedPathInfo() throws Exception {
+        interceptor.setExemptedPaths("/rest/orders");
+        request.setServletPath("/rest");
+        request.setPathInfo("/orders");
         interceptor.intercept(mai);
 
         String header = response.getHeader(COOP_HEADER);
         assertTrue("Coop header exists in exempted path", 
Strings.isEmpty(header));
     }
 
+    public void testHeaderIsSetWhenOnlyContextPathIsExempted() throws 
Exception {
+        request.setContextPath("/foo");
+        request.setServletPath("/index.action");
+        interceptor.intercept(mai);
+
+        assertEquals("Coop header is not same-origin", SAME_ORIGIN, 
response.getHeader(COOP_HEADER));
+    }
+
     public void testChangeDefaultMode() throws Exception {
         interceptor.setMode("unsafe-none");
-        request.setContextPath("/some");
+        request.setServletPath("/some.action");
         interceptor.intercept(mai);
 
         String header = response.getHeader(COOP_HEADER);
@@ -65,7 +83,7 @@ public class CoopInterceptorTest extends 
StrutsInternalTestCase {
     }
 
     public void testErrorNotRecognizedMode() {
-        request.setContextPath("/some");
+        request.setServletPath("/some.action");
 
         try{
             interceptor.setMode("foobar");
diff --git 
a/core/src/test/java/org/apache/struts2/interceptor/FetchMetadataInterceptorTest.java
 
b/core/src/test/java/org/apache/struts2/interceptor/FetchMetadataInterceptorTest.java
index 9b52c94df..189472aa7 100644
--- 
a/core/src/test/java/org/apache/struts2/interceptor/FetchMetadataInterceptorTest.java
+++ 
b/core/src/test/java/org/apache/struts2/interceptor/FetchMetadataInterceptorTest.java
@@ -33,6 +33,7 @@ import 
org.apache.struts2.config.StrutsXmlConfigurationProvider;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
 
+import javax.servlet.RequestDispatcher;
 import javax.servlet.http.HttpServletResponse;
 import java.util.Arrays;
 import java.util.Collection;
@@ -150,21 +151,46 @@ public class FetchMetadataInterceptorTest extends 
XWorkTestCase {
 
     public void testPathInExemptedPaths() throws Exception {
         request.addHeader(SEC_FETCH_SITE_HEADER, "foo");
-        request.setContextPath("/foo");
+        request.setServletPath("/foo");
 
         assertNotEquals("Expected interceptor to accept this request", 
SC_FORBIDDEN, interceptor.intercept(mai));
     }
 
     public void testPathNotInExemptedPaths() throws Exception {
         request.addHeader(SEC_FETCH_SITE_HEADER, "foo");
-        request.setContextPath("/foobar");
+        request.setServletPath("/foobar");
+
+        assertEquals("Expected interceptor to NOT accept this request", 
SC_FORBIDDEN, interceptor.intercept(mai));
+    }
+
+    public void testPathInfoInExemptedPaths() throws Exception {
+        interceptor.setExemptedPaths("/rest/orders");
+        request.addHeader(SEC_FETCH_SITE_HEADER, SITE_CROSS_SITE);
+        request.setServletPath("/rest");
+        request.setPathInfo("/orders");
+
+        assertNotEquals("Expected interceptor to accept this request", 
SC_FORBIDDEN, interceptor.intercept(mai));
+    }
+
+    public void testContextPathInExemptedPathsDoesNotExempt() throws Exception 
{
+        request.addHeader(SEC_FETCH_SITE_HEADER, SITE_CROSS_SITE);
+        request.setContextPath("/foo");
+        request.setServletPath("/index.action");
 
         assertEquals("Expected interceptor to NOT accept this request", 
SC_FORBIDDEN, interceptor.intercept(mai));
     }
 
+    public void testIncludedPathInExemptedPaths() throws Exception {
+        request.addHeader(SEC_FETCH_SITE_HEADER, SITE_CROSS_SITE);
+        request.setServletPath("/outer.action");
+        request.setAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH, "/foo");
+
+        assertNotEquals("Expected interceptor to accept this request", 
SC_FORBIDDEN, interceptor.intercept(mai));
+    }
+
     public void testVaryHeaderAcceptedReq() throws Exception {
         request.addHeader(SEC_FETCH_SITE_HEADER, "foo");
-        request.setContextPath("/foo");
+        request.setServletPath("/foo");
 
         interceptor.intercept(mai);
 
@@ -222,11 +248,11 @@ public class FetchMetadataInterceptorTest extends 
XWorkTestCase {
         FetchMetadataInterceptor configuredFetchMetadataInterceptor = 
(FetchMetadataInterceptor) 
configuredFetchMetadataInterceptorMapping.getInterceptor();
         request.removeHeader(SEC_FETCH_SITE_HEADER);
         request.addHeader(SEC_FETCH_SITE_HEADER, "foo");
-        request.setContextPath("/foo");
+        request.setServletPath("/foo");
         assertEquals("Expected interceptor to NOT accept this request [/foo]", 
SC_FORBIDDEN, configuredFetchMetadataInterceptor.intercept(mai));
-        request.setContextPath("/fetchMetadataExemptedGlobal");
+        request.setServletPath("/fetchMetadataExemptedGlobal.action");
         assertNotEquals("Expected interceptor to accept this request 
[/fetchMetadataExemptedGlobal]", SC_FORBIDDEN, 
configuredFetchMetadataInterceptor.intercept(mai));
-        request.setContextPath("/someOtherPath");
+        request.setServletPath("/someOtherPath.action");
         assertNotEquals("Expected interceptor to accept this request 
[/someOtherPath]", SC_FORBIDDEN, 
configuredFetchMetadataInterceptor.intercept(mai));
 
         // The test configuration in "struts-testing.xml" should also contain 
three actions configured differently for the "fetchMetadata" interceptor.
@@ -257,8 +283,8 @@ public class FetchMetadataInterceptorTest extends 
XWorkTestCase {
         configuredFetchMetadataInterceptor = (FetchMetadataInterceptor) 
configuredFetchMetadataInterceptorMapping.getInterceptor();
         request.removeHeader(SEC_FETCH_SITE_HEADER);
         request.addHeader(SEC_FETCH_SITE_HEADER, 
fetchMetadataExemptedActionConfig.getName());
-        request.setContextPath("/" + 
fetchMetadataExemptedActionConfig.getName());
-        assertNotEquals("Expected interceptor to accept this request [" + "/" 
+ fetchMetadataExemptedActionConfig.getName() + "]", SC_FORBIDDEN, 
configuredFetchMetadataInterceptor.intercept(mai));
+        request.setServletPath("/" + 
fetchMetadataExemptedActionConfig.getName() + ".action");
+        assertNotEquals("Expected interceptor to accept this request [" + "/" 
+ fetchMetadataExemptedActionConfig.getName() + ".action" + "]", SC_FORBIDDEN, 
configuredFetchMetadataInterceptor.intercept(mai));
 
         // Test fetchMetadata interceptor for the "fetchMetadataNotExempted" 
action.
         currentActionInterceptors = 
fetchMetadataNotExemptedActionConfig.getInterceptors();
@@ -277,8 +303,8 @@ public class FetchMetadataInterceptorTest extends 
XWorkTestCase {
         configuredFetchMetadataInterceptor = (FetchMetadataInterceptor) 
configuredFetchMetadataInterceptorMapping.getInterceptor();
         request.removeHeader(SEC_FETCH_SITE_HEADER);
         request.addHeader(SEC_FETCH_SITE_HEADER, 
fetchMetadataNotExemptedActionConfig.getName());
-        request.setContextPath("/" + 
fetchMetadataNotExemptedActionConfig.getName());
-        assertEquals("Expected interceptor to NOT accept this request [" + "/" 
+ fetchMetadataNotExemptedActionConfig.getName() + "]", SC_FORBIDDEN, 
configuredFetchMetadataInterceptor.intercept(mai));
+        request.setServletPath("/" + 
fetchMetadataNotExemptedActionConfig.getName() + ".action");
+        assertEquals("Expected interceptor to NOT accept this request [" + "/" 
+ fetchMetadataNotExemptedActionConfig.getName() + ".action" + "]", 
SC_FORBIDDEN, configuredFetchMetadataInterceptor.intercept(mai));
 
         // Test fetchMetadata interceptor for the 
"fetchMetadataExemptedGlobal" action.
         currentActionInterceptors = 
fetchMetadataExemptedGlobalActionConfig.getInterceptors();
@@ -297,8 +323,8 @@ public class FetchMetadataInterceptorTest extends 
XWorkTestCase {
         configuredFetchMetadataInterceptor = (FetchMetadataInterceptor) 
configuredFetchMetadataInterceptorMapping.getInterceptor();
         request.removeHeader(SEC_FETCH_SITE_HEADER);
         request.addHeader(SEC_FETCH_SITE_HEADER, 
fetchMetadataExemptedGlobalActionConfig.getName());
-        request.setContextPath("/" + 
fetchMetadataExemptedGlobalActionConfig.getName());
-        assertNotEquals("Expected interceptor to accept this request [" + "/" 
+ fetchMetadataExemptedGlobalActionConfig.getName() + "]", SC_FORBIDDEN, 
configuredFetchMetadataInterceptor.intercept(mai));
+        request.setServletPath("/" + 
fetchMetadataExemptedGlobalActionConfig.getName() + ".action");
+        assertNotEquals("Expected interceptor to accept this request [" + "/" 
+ fetchMetadataExemptedGlobalActionConfig.getName() + ".action" + "]", 
SC_FORBIDDEN, configuredFetchMetadataInterceptor.intercept(mai));
     }
 
 }
diff --git a/core/src/test/resources/struts-testing.xml 
b/core/src/test/resources/struts-testing.xml
index f1af997ce..a73c2c5b6 100644
--- a/core/src/test/resources/struts-testing.xml
+++ b/core/src/test/resources/struts-testing.xml
@@ -28,21 +28,21 @@
         <interceptors>
             <interceptor-stack name="defaultInterceptorStack">
                 <interceptor-ref name="defaultStack">
-                    <param 
name="fetchMetadata.setExemptedPaths">/fetchMetadataExemptedGlobal,/someOtherPath</param>
+                    <param 
name="fetchMetadata.setExemptedPaths">/fetchMetadataExemptedGlobal.action,/someOtherPath.action</param>
                 </interceptor-ref>
             </interceptor-stack>
         </interceptors>
 
         <action name="fetchMetadataExempted" 
class="com.opensymphony.xwork2.SimpleAction">
             <interceptor-ref name="defaultInterceptorStack">
-               <param 
name="fetchMetadata.setExemptedPaths">/fetchMetadataExempted</param>
+               <param 
name="fetchMetadata.setExemptedPaths">/fetchMetadataExempted.action</param>
             </interceptor-ref>
             <result name="success">hello.jsp</result>
         </action>
 
         <action name="fetchMetadataNotExempted" 
class="com.opensymphony.xwork2.SimpleAction">
             <interceptor-ref name="defaultInterceptorStack">
-                <param 
name="fetchMetadata.setExemptedPaths">/nonMatchingActionPath</param>
+                <param 
name="fetchMetadata.setExemptedPaths">/nonMatchingActionPath.action</param>
             </interceptor-ref>
             <result name="success">hello.jsp</result>
         </action>

Reply via email to