From 520b0b07946e0cfe01ad7fabcc822bb9ef47bd5a Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Sat, 7 Nov 2026 17:42:23 +0100 Subject: [PATCH] WW-5754 fix(core): accept cross-site HEAD navigations in the Fetch Metadata policy 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 --- .../StrutsResourceIsolationPolicy.java | 4 ++- .../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 f1b786b0d0..94cf18b2f0 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 @@ private boolean isAllowedTopLevelNavigation(HttpServletRequest request) { 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 0191c626f2..15bb244e05 100644 --- a/core/src/test/java/org/apache/struts2/interceptor/FetchMetadataInterceptorTest.java +++ b/core/src/test/java/org/apache/struts2/interceptor/FetchMetadataInterceptorTest.java @@ -96,21 +96,18 @@ public void testValidSite() throws Exception { } - 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 {