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 370ac390f WW-5754 fix(core): accept cross-site HEAD navigations in the
Fetch Metadata policy (#2123)
370ac390f is described below
commit 370ac390f9964c16b708a8ae4ca1df499c4e8c3e
Author: Lukasz Lenart <[email protected]>
AuthorDate: Sat Nov 7 18:30:34 2026 +0100
WW-5754 fix(core): accept cross-site HEAD navigations in the Fetch Metadata
policy (#2123)
The documented policy lists HEAD alongside GET as an accepted method for
cross-site navigations; WW-5753 accepted GET only.
Cross-site POST navigations stay rejected: allowing them would re-open
classic form-POST CSRF. Endpoints receiving SAML/OIDC form_post or
payment-gateway returns opt out per action with the fetchMetadata
interceptor's "disabled" parameter.
Replaces testValidTopLevelNavigation, which sent no Sec-Fetch-Site header
and so never reached the navigation branch.
Co-authored-by: Claude Opus 5.5 <[email protected]>
---
.../interceptor/StrutsResourceIsolationPolicy.java | 4 +++-
.../interceptor/FetchMetadataInterceptorTest.java | 25 ++++++++++------------
2 files changed, 14 insertions(+), 15 deletions(-)
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 146b493d2..6be10abbe 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,9 @@ 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());
+ String method = request.getMethod();
+ boolean isSimpleTopLevelNavigation =
MODE_NAVIGATE.equalsIgnoreCase(mode)
+ && ("GET".equalsIgnoreCase(method) ||
"HEAD".equalsIgnoreCase(method));
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 f60e98d28..9b52c94df 100644
---
a/core/src/test/java/org/apache/struts2/interceptor/FetchMetadataInterceptorTest.java
+++
b/core/src/test/java/org/apache/struts2/interceptor/FetchMetadataInterceptorTest.java
@@ -95,21 +95,18 @@ public class FetchMetadataInterceptorTest extends
XWorkTestCase {
}
- public void testValidTopLevelNavigation() throws Exception {
- request.addHeader(SEC_FETCH_MODE_HEADER, MODE_NAVIGATE);
- request.addHeader(SEC_FETCH_DEST_HEADER, DEST_SCRIPT);
- request.setMethod("GET");
-
- 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");
+ public void testCrossSiteTopLevelGetAndHeadNavigationAllowed() throws
Exception {
+ for (String method : Arrays.asList("GET", "HEAD")) {
+ 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);
- assertNotEquals("Expected interceptor to accept this request",
SC_FORBIDDEN, interceptor.intercept(mai));
+ assertNotEquals("Expected interceptor to accept cross-site " +
method + " navigation", SC_FORBIDDEN, interceptor.intercept(mai));
+ }
}
public void testCrossSiteNavigationWithUnsafeMethodRejected() throws
Exception {