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 020bf7211 WW-5753 Reject cross-site non-GET navigations and cross-site
GET subresource requests in the Fetch Metadata policy (#1983) (#1984)
020bf7211 is described below
commit 020bf721116092bff63c163aa9ece31b1439e5d4
Author: Lukasz Lenart <[email protected]>
AuthorDate: Thu Oct 1 20:50:24 2026 +0200
WW-5753 Reject cross-site non-GET navigations and cross-site GET
subresource requests in the Fetch Metadata policy (#1983) (#1984)
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.
(cherry picked from commit 1d92b07b06c3b91ad5a4febc08b1800d0dec29fe)
Co-authored-by: ⳕⲛτⲉⲅⲥⲉⳏτⲟⲅ 🕵🏻 <[email protected]>
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 24e0e7f8d..146b493d2 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 6426ebeaa..f60e98d28 100644
---
a/core/src/test/java/org/apache/struts2/interceptor/FetchMetadataInterceptorTest.java
+++
b/core/src/test/java/org/apache/struts2/interceptor/FetchMetadataInterceptorTest.java
@@ -39,14 +39,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;
@@ -97,6 +103,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");