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

lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git


The following commit(s) were added to refs/heads/main by this push:
     new 1d92b07b0 WW-5753 Reject cross-site non-GET navigations and cross-site 
GET subresource requests in the Fetch Metadata policy (#1983)
1d92b07b0 is described below

commit 1d92b07b06c3b91ad5a4febc08b1800d0dec29fe
Author: ⳕⲛτⲉⲅⲥⲉⳏτⲟⲅ 🕵🏻 <[email protected]>
AuthorDate: Fri Oct 2 00:00:07 2026 +0530

    WW-5753 Reject cross-site non-GET navigations and cross-site GET 
subresource requests in the Fetch Metadata policy (#1983)
    
    A cross-site request was accepted when it was a navigation or a GET. The 
documented policy and the Fetch Metadata reference policy require a navigation 
and a GET.
    
    Co-authored-by: Gouri Sankar A <[email protected]>
---
 .../interceptor/StrutsResourceIsolationPolicy.java |  2 +-
 .../interceptor/FetchMetadataInterceptorTest.java  | 43 ++++++++++++++++++++++
 2 files changed, 44 insertions(+), 1 deletion(-)

diff --git 
a/core/src/main/java/org/apache/struts2/interceptor/StrutsResourceIsolationPolicy.java
 
b/core/src/main/java/org/apache/struts2/interceptor/StrutsResourceIsolationPolicy.java
index 411e330ae..f1b786b0d 100644
--- 
a/core/src/main/java/org/apache/struts2/interceptor/StrutsResourceIsolationPolicy.java
+++ 
b/core/src/main/java/org/apache/struts2/interceptor/StrutsResourceIsolationPolicy.java
@@ -56,7 +56,7 @@ public final class StrutsResourceIsolationPolicy implements 
ResourceIsolationPol
         String mode = request.getHeader(SEC_FETCH_MODE_HEADER);
         String dest = request.getHeader(SEC_FETCH_DEST_HEADER);
 
-        boolean isSimpleTopLevelNavigation = 
MODE_NAVIGATE.equalsIgnoreCase(mode) || 
"GET".equalsIgnoreCase(request.getMethod());
+        boolean isSimpleTopLevelNavigation = 
MODE_NAVIGATE.equalsIgnoreCase(mode) && 
"GET".equalsIgnoreCase(request.getMethod());
         boolean isNotObjectOrEmbedRequest = !DEST_EMBED.equalsIgnoreCase(dest) 
&& !DEST_OBJECT.equalsIgnoreCase(dest);
 
         return isSimpleTopLevelNavigation && isNotObjectOrEmbedRequest;
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 8e2c2296d..0191c626f 100644
--- 
a/core/src/test/java/org/apache/struts2/interceptor/FetchMetadataInterceptorTest.java
+++ 
b/core/src/test/java/org/apache/struts2/interceptor/FetchMetadataInterceptorTest.java
@@ -40,14 +40,20 @@ import java.util.Collection;
 import java.util.Iterator;
 import java.util.Objects;
 
+import static 
org.apache.struts2.interceptor.ResourceIsolationPolicy.DEST_DOCUMENT;
+import static 
org.apache.struts2.interceptor.ResourceIsolationPolicy.DEST_EMPTY;
+import static 
org.apache.struts2.interceptor.ResourceIsolationPolicy.DEST_IMAGE;
 import static 
org.apache.struts2.interceptor.ResourceIsolationPolicy.DEST_EMBED;
 import static 
org.apache.struts2.interceptor.ResourceIsolationPolicy.DEST_OBJECT;
 import static 
org.apache.struts2.interceptor.ResourceIsolationPolicy.DEST_SCRIPT;
+import static org.apache.struts2.interceptor.ResourceIsolationPolicy.MODE_CORS;
+import static 
org.apache.struts2.interceptor.ResourceIsolationPolicy.MODE_NO_CORS;
 import static 
org.apache.struts2.interceptor.ResourceIsolationPolicy.MODE_NAVIGATE;
 import static 
org.apache.struts2.interceptor.ResourceIsolationPolicy.SEC_FETCH_DEST_HEADER;
 import static 
org.apache.struts2.interceptor.ResourceIsolationPolicy.SEC_FETCH_MODE_HEADER;
 import static 
org.apache.struts2.interceptor.ResourceIsolationPolicy.SEC_FETCH_SITE_HEADER;
 import static 
org.apache.struts2.interceptor.ResourceIsolationPolicy.SEC_FETCH_USER_HEADER;
+import static 
org.apache.struts2.interceptor.ResourceIsolationPolicy.SITE_CROSS_SITE;
 import static org.apache.struts2.interceptor.ResourceIsolationPolicy.SITE_NONE;
 import static 
org.apache.struts2.interceptor.ResourceIsolationPolicy.SITE_SAME_ORIGIN;
 import static 
org.apache.struts2.interceptor.ResourceIsolationPolicy.SITE_SAME_SITE;
@@ -98,6 +104,43 @@ public class FetchMetadataInterceptorTest extends 
XWorkTestCase {
         assertNotEquals("Expected interceptor to accept this request", 
SC_FORBIDDEN, interceptor.intercept(mai));
     }
 
+    public void testCrossSiteTopLevelGetNavigationAllowed() throws Exception {
+        request.addHeader(SEC_FETCH_SITE_HEADER, SITE_CROSS_SITE);
+        request.addHeader(SEC_FETCH_MODE_HEADER, MODE_NAVIGATE);
+        request.addHeader(SEC_FETCH_DEST_HEADER, DEST_DOCUMENT);
+        request.setMethod("GET");
+
+        assertNotEquals("Expected interceptor to accept this request", 
SC_FORBIDDEN, interceptor.intercept(mai));
+    }
+
+    public void testCrossSiteNavigationWithUnsafeMethodRejected() throws 
Exception {
+        for (String method : Arrays.asList("POST", "PUT", "DELETE", "PATCH")) {
+            request.removeHeader(SEC_FETCH_SITE_HEADER);
+            request.addHeader(SEC_FETCH_SITE_HEADER, SITE_CROSS_SITE);
+            request.removeHeader(SEC_FETCH_MODE_HEADER);
+            request.addHeader(SEC_FETCH_MODE_HEADER, MODE_NAVIGATE);
+            request.removeHeader(SEC_FETCH_DEST_HEADER);
+            request.addHeader(SEC_FETCH_DEST_HEADER, DEST_DOCUMENT);
+            request.setMethod(method);
+
+            assertEquals("Expected interceptor to NOT accept cross-site " + 
method + " navigation", SC_FORBIDDEN, interceptor.intercept(mai));
+        }
+    }
+
+    public void testCrossSiteGetSubresourceRequestRejected() throws Exception {
+        for (String[] modeAndDest : new String[][]{{MODE_NO_CORS, 
DEST_SCRIPT}, {MODE_NO_CORS, DEST_IMAGE}, {MODE_CORS, DEST_EMPTY}}) {
+            request.removeHeader(SEC_FETCH_SITE_HEADER);
+            request.addHeader(SEC_FETCH_SITE_HEADER, SITE_CROSS_SITE);
+            request.removeHeader(SEC_FETCH_MODE_HEADER);
+            request.addHeader(SEC_FETCH_MODE_HEADER, modeAndDest[0]);
+            request.removeHeader(SEC_FETCH_DEST_HEADER);
+            request.addHeader(SEC_FETCH_DEST_HEADER, modeAndDest[1]);
+            request.setMethod("GET");
+
+            assertEquals("Expected interceptor to NOT accept cross-site GET " 
+ modeAndDest[0] + "/" + modeAndDest[1], SC_FORBIDDEN, 
interceptor.intercept(mai));
+        }
+    }
+
     public void testInvalidTopLevelNavigation() throws Exception {
         for (String header : Arrays.asList(DEST_OBJECT, DEST_EMBED)) {
             request.addHeader(SEC_FETCH_SITE_HEADER, "foo");

Reply via email to