From 242a779034a06cd0a825cf968e6c303aada0ed2a Mon Sep 17 00:00:00 2001
From: Robert Winch <362503+rwinch@users.noreply.github.com>
Date: Wed, 9 Sep 2026 13:54:54 -0500
Subject: [PATCH 1/4] Ignore Apple touch icon requests in default RequestCache
Safari and other WebKit-based browsers automatically probe for
apple-touch-icon.png, apple-touch-icon-precomposed.png, and sized
variants (e.g. apple-touch-icon-152x152.png) at the domain root as
soon as a page loads, even without a matching tag. This is a
background fetch unrelated to what the user actually navigated to, so
it should never become the URL a user is redirected to after
authenticating, just like the existing favicon.* exclusion.
This also refactors the single-pattern favicon matcher into a small
list of ignored background-request patterns so more patterns can be
added going forward.
Closes gh-19692
---
.../configurers/RequestCacheConfigurer.java | 25 +++++++++--
.../RequestCacheConfigurerTests.java | 44 +++++++++++++++++++
.../CookieServerRequestCache.java | 18 ++++++--
.../WebSessionServerRequestCache.java | 18 ++++++--
.../CookieServerRequestCacheTests.java | 27 ++++++++++++
.../WebSessionServerRequestCacheTests.java | 27 ++++++++++++
6 files changed, 149 insertions(+), 10 deletions(-)
diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java
index e280efc85c..86c8f21dd3 100644
--- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java
+++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java
@@ -32,6 +32,7 @@ import org.springframework.security.web.savedrequest.RequestCacheAwareFilter;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
+import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.web.accept.ContentNegotiationStrategy;
@@ -140,7 +141,7 @@ public final class RequestCacheConfigurer>
@SuppressWarnings("unchecked")
private RequestMatcher createDefaultSavedRequestMatcher(H http) {
- RequestMatcher notFavIcon = new NegatedRequestMatcher(getFaviconRequestMatcher());
+ RequestMatcher notIgnoredBackgroundRequest = new NegatedRequestMatcher(getIgnoredBackgroundRequestMatcher());
RequestMatcher notXRequestedWith = new NegatedRequestMatcher(
new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest"));
RequestMatcher notWebSocket = new NegatedRequestMatcher(
@@ -152,7 +153,7 @@ public final class RequestCacheConfigurer>
RequestMatcher getRequests = getRequestMatcherBuilder().matcher(HttpMethod.GET, "/**");
matchers.add(0, getRequests);
}
- matchers.add(notFavIcon);
+ matchers.add(notIgnoredBackgroundRequest);
matchers.add(notMatchingMediaType(http, MediaType.APPLICATION_JSON));
matchers.add(notXRequestedWith);
matchers.add(notMatchingMediaType(http, MediaType.MULTIPART_FORM_DATA));
@@ -171,8 +172,24 @@ public final class RequestCacheConfigurer>
return new NegatedRequestMatcher(mediaRequest);
}
- private RequestMatcher getFaviconRequestMatcher() {
- return getRequestMatcherBuilder().matcher("/favicon.*");
+ /**
+ * Path patterns for requests that browsers request automatically in the background
+ * (e.g. to display an icon), and that should therefore never be saved as the URL to
+ * redirect to after authentication succeeds.
+ */
+ private static final String[] IGNORED_BACKGROUND_REQUEST_PATTERNS = { "/favicon.*",
+ // Safari/WebKit request apple-touch-icon.png,
+ // apple-touch-icon-precomposed.png,
+ // and sized variants (e.g. apple-touch-icon-152x152.png) even without a
+ // matching tag in the page
+ "/apple-touch-icon*.png" };
+
+ private RequestMatcher getIgnoredBackgroundRequestMatcher() {
+ List matchers = new ArrayList<>();
+ for (String pattern : IGNORED_BACKGROUND_REQUEST_PATTERNS) {
+ matchers.add(getRequestMatcherBuilder().matcher(pattern));
+ }
+ return new OrRequestMatcher(matchers);
}
}
diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java
index cce83b88b5..2aa5b10be8 100644
--- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java
+++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java
@@ -114,6 +114,50 @@ public class RequestCacheConfigurerTests {
this.mvc.perform(formLogin(session)).andExpect(redirectedUrl("/"));
}
+ @Test
+ public void getWhenBookmarkedUrlIsAppleTouchIconThenPostAuthenticationRedirectsToRoot() throws Exception {
+ this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire();
+ // @formatter:off
+ MockHttpSession session = (MockHttpSession) this.mvc.perform(get("/apple-touch-icon.png"))
+ .andExpect(redirectedUrl("/login"))
+ .andReturn()
+ .getRequest()
+ .getSession();
+ // @formatter:on
+ // ignores apple-touch-icon.png
+ this.mvc.perform(formLogin(session)).andExpect(redirectedUrl("/"));
+ }
+
+ @Test
+ public void getWhenBookmarkedUrlIsAppleTouchIconPrecomposedThenPostAuthenticationRedirectsToRoot()
+ throws Exception {
+ this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire();
+ // @formatter:off
+ MockHttpSession session = (MockHttpSession) this.mvc.perform(get("/apple-touch-icon-precomposed.png"))
+ .andExpect(redirectedUrl("/login"))
+ .andReturn()
+ .getRequest()
+ .getSession();
+ // @formatter:on
+ // ignores apple-touch-icon-precomposed.png
+ this.mvc.perform(formLogin(session)).andExpect(redirectedUrl("/"));
+ }
+
+ @Test
+ public void getWhenBookmarkedUrlIsAppleTouchIconSizedPrecomposedThenPostAuthenticationRedirectsToRoot()
+ throws Exception {
+ this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire();
+ // @formatter:off
+ MockHttpSession session = (MockHttpSession) this.mvc.perform(get("/apple-touch-icon-152x152-precomposed.png"))
+ .andExpect(redirectedUrl("/login"))
+ .andReturn()
+ .getRequest()
+ .getSession();
+ // @formatter:on
+ // ignores apple-touch-icon-152x152-precomposed.png
+ this.mvc.perform(formLogin(session)).andExpect(redirectedUrl("/"));
+ }
+
// SEC-2321
@Test
public void getWhenBookmarkedRequestIsApplicationJsonThenPostAuthenticationRedirectsToRoot() throws Exception {
diff --git a/web/src/main/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCache.java b/web/src/main/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCache.java
index 1f498ba30c..9005930970 100644
--- a/web/src/main/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCache.java
+++ b/web/src/main/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCache.java
@@ -158,13 +158,25 @@ public class CookieServerRequestCache implements ServerRequestCache {
StandardCharsets.UTF_8);
}
+ /**
+ * Path patterns for requests that browsers request automatically in the background
+ * (e.g. to display an icon), and that should therefore never be saved as the URL to
+ * redirect to after authentication succeeds.
+ */
+ private static final String[] IGNORED_BACKGROUND_REQUEST_PATTERNS = { "/favicon.*",
+ // Safari/WebKit request apple-touch-icon.png,
+ // apple-touch-icon-precomposed.png,
+ // and sized variants (e.g. apple-touch-icon-152x152.png) even without a
+ // matching tag in the page
+ "/apple-touch-icon*.png" };
+
private static ServerWebExchangeMatcher createDefaultRequestMatcher() {
ServerWebExchangeMatcher get = ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/**");
- ServerWebExchangeMatcher notFavicon = new NegatedServerWebExchangeMatcher(
- ServerWebExchangeMatchers.pathMatchers("/favicon.*"));
+ ServerWebExchangeMatcher notIgnoredBackgroundRequest = new NegatedServerWebExchangeMatcher(
+ ServerWebExchangeMatchers.pathMatchers(IGNORED_BACKGROUND_REQUEST_PATTERNS));
MediaTypeServerWebExchangeMatcher html = new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML);
html.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
- return new AndServerWebExchangeMatcher(get, notFavicon, html);
+ return new AndServerWebExchangeMatcher(get, notIgnoredBackgroundRequest, html);
}
}
diff --git a/web/src/main/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCache.java b/web/src/main/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCache.java
index 723e20dcdf..1b13bc1d6b 100644
--- a/web/src/main/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCache.java
+++ b/web/src/main/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCache.java
@@ -158,13 +158,25 @@ public class WebSessionServerRequestCache implements ServerRequestCache {
// @formatter:on
}
+ /**
+ * Path patterns for requests that browsers request automatically in the background
+ * (e.g. to display an icon), and that should therefore never be saved as the URL to
+ * redirect to after authentication succeeds.
+ */
+ private static final String[] IGNORED_BACKGROUND_REQUEST_PATTERNS = { "/favicon.*",
+ // Safari/WebKit request apple-touch-icon.png,
+ // apple-touch-icon-precomposed.png,
+ // and sized variants (e.g. apple-touch-icon-152x152.png) even without a
+ // matching tag in the page
+ "/apple-touch-icon*.png" };
+
private static ServerWebExchangeMatcher createDefaultRequestMatcher() {
ServerWebExchangeMatcher get = ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/**");
- ServerWebExchangeMatcher notFavicon = new NegatedServerWebExchangeMatcher(
- ServerWebExchangeMatchers.pathMatchers("/favicon.*"));
+ ServerWebExchangeMatcher notIgnoredBackgroundRequest = new NegatedServerWebExchangeMatcher(
+ ServerWebExchangeMatchers.pathMatchers(IGNORED_BACKGROUND_REQUEST_PATTERNS));
MediaTypeServerWebExchangeMatcher html = new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML);
html.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
- return new AndServerWebExchangeMatcher(get, notFavicon, html);
+ return new AndServerWebExchangeMatcher(get, notIgnoredBackgroundRequest, html);
}
}
diff --git a/web/src/test/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCacheTests.java b/web/src/test/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCacheTests.java
index 1c3808d176..4c17c669c5 100644
--- a/web/src/test/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCacheTests.java
+++ b/web/src/test/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCacheTests.java
@@ -77,6 +77,33 @@ public class CookieServerRequestCacheTests {
assertThat(cookies).isEmpty();
}
+ @Test
+ public void saveRequestWhenGetRequestAppleTouchIconThenNoCookie() {
+ MockServerWebExchange exchange = MockServerWebExchange
+ .from(MockServerHttpRequest.get("/apple-touch-icon.png").accept(MediaType.TEXT_HTML));
+ this.cache.saveRequest(exchange).block();
+ MultiValueMap cookies = exchange.getResponse().getCookies();
+ assertThat(cookies).isEmpty();
+ }
+
+ @Test
+ public void saveRequestWhenGetRequestAppleTouchIconPrecomposedThenNoCookie() {
+ MockServerWebExchange exchange = MockServerWebExchange
+ .from(MockServerHttpRequest.get("/apple-touch-icon-precomposed.png").accept(MediaType.TEXT_HTML));
+ this.cache.saveRequest(exchange).block();
+ MultiValueMap cookies = exchange.getResponse().getCookies();
+ assertThat(cookies).isEmpty();
+ }
+
+ @Test
+ public void saveRequestWhenGetRequestAppleTouchIconSizedPrecomposedThenNoCookie() {
+ MockServerWebExchange exchange = MockServerWebExchange
+ .from(MockServerHttpRequest.get("/apple-touch-icon-152x152-precomposed.png").accept(MediaType.TEXT_HTML));
+ this.cache.saveRequest(exchange).block();
+ MultiValueMap cookies = exchange.getResponse().getCookies();
+ assertThat(cookies).isEmpty();
+ }
+
@Test
public void saveRequestWhenPostRequestThenNoCookie() {
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.post("/secured/"));
diff --git a/web/src/test/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCacheTests.java b/web/src/test/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCacheTests.java
index d383cde5fe..d1f6e647ca 100644
--- a/web/src/test/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCacheTests.java
+++ b/web/src/test/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCacheTests.java
@@ -68,6 +68,33 @@ public class WebSessionServerRequestCacheTests {
assertThat(saved).isNull();
}
+ @Test
+ public void saveRequestGetRequestWhenAppleTouchIconThenNotFound() {
+ MockServerWebExchange exchange = MockServerWebExchange
+ .from(MockServerHttpRequest.get("/apple-touch-icon.png").accept(MediaType.TEXT_HTML));
+ this.cache.saveRequest(exchange).block();
+ URI saved = this.cache.getRedirectUri(exchange).block();
+ assertThat(saved).isNull();
+ }
+
+ @Test
+ public void saveRequestGetRequestWhenAppleTouchIconPrecomposedThenNotFound() {
+ MockServerWebExchange exchange = MockServerWebExchange
+ .from(MockServerHttpRequest.get("/apple-touch-icon-precomposed.png").accept(MediaType.TEXT_HTML));
+ this.cache.saveRequest(exchange).block();
+ URI saved = this.cache.getRedirectUri(exchange).block();
+ assertThat(saved).isNull();
+ }
+
+ @Test
+ public void saveRequestGetRequestWhenAppleTouchIconSizedPrecomposedThenNotFound() {
+ MockServerWebExchange exchange = MockServerWebExchange
+ .from(MockServerHttpRequest.get("/apple-touch-icon-152x152-precomposed.png").accept(MediaType.TEXT_HTML));
+ this.cache.saveRequest(exchange).block();
+ URI saved = this.cache.getRedirectUri(exchange).block();
+ assertThat(saved).isNull();
+ }
+
@Test
public void saveRequestGetRequestWhenPostThenNotFound() {
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.post("/secured/"));
From d6363f2d3e774021aa304805cbe0e57f049e3d34 Mon Sep 17 00:00:00 2001
From: Robert Winch <362503+rwinch@users.noreply.github.com>
Date: Wed, 9 Sep 2026 14:00:19 -0500
Subject: [PATCH 2/4] Ignore web app manifest requests in default RequestCache
Chrome, Edge, and other Chromium-based browsers asynchronously fetch
a page's web app manifest (manifest.json or manifest.webmanifest)
shortly after page load to evaluate PWA installability. This is a
fire-and-forget background request that should never become the URL
a user is redirected to after authenticating.
Closes gh-19695
---
.../configurers/RequestCacheConfigurer.java | 5 +++-
.../RequestCacheConfigurerTests.java | 28 +++++++++++++++++++
.../CookieServerRequestCache.java | 5 +++-
.../WebSessionServerRequestCache.java | 5 +++-
.../CookieServerRequestCacheTests.java | 18 ++++++++++++
.../WebSessionServerRequestCacheTests.java | 18 ++++++++++++
6 files changed, 76 insertions(+), 3 deletions(-)
diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java
index 86c8f21dd3..662a01ac81 100644
--- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java
+++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java
@@ -182,7 +182,10 @@ public final class RequestCacheConfigurer>
// apple-touch-icon-precomposed.png,
// and sized variants (e.g. apple-touch-icon-152x152.png) even without a
// matching tag in the page
- "/apple-touch-icon*.png" };
+ "/apple-touch-icon*.png",
+ // Chromium browsers fetch the web app manifest in the background to
+ // evaluate PWA installability
+ "/manifest.json", "/manifest.webmanifest" };
private RequestMatcher getIgnoredBackgroundRequestMatcher() {
List matchers = new ArrayList<>();
diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java
index 2aa5b10be8..6ecffb7c25 100644
--- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java
+++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java
@@ -158,6 +158,34 @@ public class RequestCacheConfigurerTests {
this.mvc.perform(formLogin(session)).andExpect(redirectedUrl("/"));
}
+ @Test
+ public void getWhenBookmarkedUrlIsManifestJsonThenPostAuthenticationRedirectsToRoot() throws Exception {
+ this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire();
+ // @formatter:off
+ MockHttpSession session = (MockHttpSession) this.mvc.perform(get("/manifest.json"))
+ .andExpect(redirectedUrl("/login"))
+ .andReturn()
+ .getRequest()
+ .getSession();
+ // @formatter:on
+ // ignores manifest.json
+ this.mvc.perform(formLogin(session)).andExpect(redirectedUrl("/"));
+ }
+
+ @Test
+ public void getWhenBookmarkedUrlIsManifestWebmanifestThenPostAuthenticationRedirectsToRoot() throws Exception {
+ this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire();
+ // @formatter:off
+ MockHttpSession session = (MockHttpSession) this.mvc.perform(get("/manifest.webmanifest"))
+ .andExpect(redirectedUrl("/login"))
+ .andReturn()
+ .getRequest()
+ .getSession();
+ // @formatter:on
+ // ignores manifest.webmanifest
+ this.mvc.perform(formLogin(session)).andExpect(redirectedUrl("/"));
+ }
+
// SEC-2321
@Test
public void getWhenBookmarkedRequestIsApplicationJsonThenPostAuthenticationRedirectsToRoot() throws Exception {
diff --git a/web/src/main/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCache.java b/web/src/main/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCache.java
index 9005930970..25d9dba0fd 100644
--- a/web/src/main/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCache.java
+++ b/web/src/main/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCache.java
@@ -168,7 +168,10 @@ public class CookieServerRequestCache implements ServerRequestCache {
// apple-touch-icon-precomposed.png,
// and sized variants (e.g. apple-touch-icon-152x152.png) even without a
// matching tag in the page
- "/apple-touch-icon*.png" };
+ "/apple-touch-icon*.png",
+ // Chromium browsers fetch the web app manifest in the background to
+ // evaluate PWA installability
+ "/manifest.json", "/manifest.webmanifest" };
private static ServerWebExchangeMatcher createDefaultRequestMatcher() {
ServerWebExchangeMatcher get = ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/**");
diff --git a/web/src/main/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCache.java b/web/src/main/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCache.java
index 1b13bc1d6b..279c6d865d 100644
--- a/web/src/main/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCache.java
+++ b/web/src/main/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCache.java
@@ -168,7 +168,10 @@ public class WebSessionServerRequestCache implements ServerRequestCache {
// apple-touch-icon-precomposed.png,
// and sized variants (e.g. apple-touch-icon-152x152.png) even without a
// matching tag in the page
- "/apple-touch-icon*.png" };
+ "/apple-touch-icon*.png",
+ // Chromium browsers fetch the web app manifest in the background to
+ // evaluate PWA installability
+ "/manifest.json", "/manifest.webmanifest" };
private static ServerWebExchangeMatcher createDefaultRequestMatcher() {
ServerWebExchangeMatcher get = ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/**");
diff --git a/web/src/test/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCacheTests.java b/web/src/test/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCacheTests.java
index 4c17c669c5..75624d58c7 100644
--- a/web/src/test/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCacheTests.java
+++ b/web/src/test/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCacheTests.java
@@ -104,6 +104,24 @@ public class CookieServerRequestCacheTests {
assertThat(cookies).isEmpty();
}
+ @Test
+ public void saveRequestWhenGetRequestManifestJsonThenNoCookie() {
+ MockServerWebExchange exchange = MockServerWebExchange
+ .from(MockServerHttpRequest.get("/manifest.json").accept(MediaType.TEXT_HTML));
+ this.cache.saveRequest(exchange).block();
+ MultiValueMap cookies = exchange.getResponse().getCookies();
+ assertThat(cookies).isEmpty();
+ }
+
+ @Test
+ public void saveRequestWhenGetRequestManifestWebmanifestThenNoCookie() {
+ MockServerWebExchange exchange = MockServerWebExchange
+ .from(MockServerHttpRequest.get("/manifest.webmanifest").accept(MediaType.TEXT_HTML));
+ this.cache.saveRequest(exchange).block();
+ MultiValueMap cookies = exchange.getResponse().getCookies();
+ assertThat(cookies).isEmpty();
+ }
+
@Test
public void saveRequestWhenPostRequestThenNoCookie() {
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.post("/secured/"));
diff --git a/web/src/test/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCacheTests.java b/web/src/test/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCacheTests.java
index d1f6e647ca..4b68546025 100644
--- a/web/src/test/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCacheTests.java
+++ b/web/src/test/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCacheTests.java
@@ -95,6 +95,24 @@ public class WebSessionServerRequestCacheTests {
assertThat(saved).isNull();
}
+ @Test
+ public void saveRequestGetRequestWhenManifestJsonThenNotFound() {
+ MockServerWebExchange exchange = MockServerWebExchange
+ .from(MockServerHttpRequest.get("/manifest.json").accept(MediaType.TEXT_HTML));
+ this.cache.saveRequest(exchange).block();
+ URI saved = this.cache.getRedirectUri(exchange).block();
+ assertThat(saved).isNull();
+ }
+
+ @Test
+ public void saveRequestGetRequestWhenManifestWebmanifestThenNotFound() {
+ MockServerWebExchange exchange = MockServerWebExchange
+ .from(MockServerHttpRequest.get("/manifest.webmanifest").accept(MediaType.TEXT_HTML));
+ this.cache.saveRequest(exchange).block();
+ URI saved = this.cache.getRedirectUri(exchange).block();
+ assertThat(saved).isNull();
+ }
+
@Test
public void saveRequestGetRequestWhenPostThenNotFound() {
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.post("/secured/"));
From bf30738840e40cadaa54c1c58953717f4d4f6abf Mon Sep 17 00:00:00 2001
From: Robert Winch <362503+rwinch@users.noreply.github.com>
Date: Wed, 9 Sep 2026 14:08:39 -0500
Subject: [PATCH 3/4] Ignore browserconfig.xml requests in default RequestCache
Legacy Internet Explorer 11 and pre-Chromium Edge automatically fetch
browserconfig.xml at the domain root to configure Windows pinned-site
tiles. It's still commonly generated by favicon-generator tooling even
though it's legacy, and is a background request unrelated to what the
user actually navigated to, so it shouldn't be able to become the URL
a user is redirected to after authenticating.
Closes gh-19693
---
.../web/configurers/RequestCacheConfigurer.java | 5 ++++-
.../configurers/RequestCacheConfigurerTests.java | 14 ++++++++++++++
.../savedrequest/CookieServerRequestCache.java | 5 ++++-
.../savedrequest/WebSessionServerRequestCache.java | 5 ++++-
.../CookieServerRequestCacheTests.java | 9 +++++++++
.../WebSessionServerRequestCacheTests.java | 9 +++++++++
6 files changed, 44 insertions(+), 3 deletions(-)
diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java
index 662a01ac81..4002a3b9f4 100644
--- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java
+++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java
@@ -185,7 +185,10 @@ public final class RequestCacheConfigurer>
"/apple-touch-icon*.png",
// Chromium browsers fetch the web app manifest in the background to
// evaluate PWA installability
- "/manifest.json", "/manifest.webmanifest" };
+ "/manifest.json", "/manifest.webmanifest",
+ // Legacy IE11/Edge fetch browserconfig.xml in the background to
+ // configure pinned-site tiles
+ "/browserconfig.xml" };
private RequestMatcher getIgnoredBackgroundRequestMatcher() {
List matchers = new ArrayList<>();
diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java
index 6ecffb7c25..48c7304ea3 100644
--- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java
+++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java
@@ -186,6 +186,20 @@ public class RequestCacheConfigurerTests {
this.mvc.perform(formLogin(session)).andExpect(redirectedUrl("/"));
}
+ @Test
+ public void getWhenBookmarkedUrlIsBrowserConfigXmlThenPostAuthenticationRedirectsToRoot() throws Exception {
+ this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire();
+ // @formatter:off
+ MockHttpSession session = (MockHttpSession) this.mvc.perform(get("/browserconfig.xml"))
+ .andExpect(redirectedUrl("/login"))
+ .andReturn()
+ .getRequest()
+ .getSession();
+ // @formatter:on
+ // ignores browserconfig.xml
+ this.mvc.perform(formLogin(session)).andExpect(redirectedUrl("/"));
+ }
+
// SEC-2321
@Test
public void getWhenBookmarkedRequestIsApplicationJsonThenPostAuthenticationRedirectsToRoot() throws Exception {
diff --git a/web/src/main/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCache.java b/web/src/main/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCache.java
index 25d9dba0fd..4361bf7dc3 100644
--- a/web/src/main/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCache.java
+++ b/web/src/main/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCache.java
@@ -171,7 +171,10 @@ public class CookieServerRequestCache implements ServerRequestCache {
"/apple-touch-icon*.png",
// Chromium browsers fetch the web app manifest in the background to
// evaluate PWA installability
- "/manifest.json", "/manifest.webmanifest" };
+ "/manifest.json", "/manifest.webmanifest",
+ // Legacy IE11/Edge fetch browserconfig.xml in the background to
+ // configure pinned-site tiles
+ "/browserconfig.xml" };
private static ServerWebExchangeMatcher createDefaultRequestMatcher() {
ServerWebExchangeMatcher get = ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/**");
diff --git a/web/src/main/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCache.java b/web/src/main/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCache.java
index 279c6d865d..b3f72559ae 100644
--- a/web/src/main/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCache.java
+++ b/web/src/main/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCache.java
@@ -171,7 +171,10 @@ public class WebSessionServerRequestCache implements ServerRequestCache {
"/apple-touch-icon*.png",
// Chromium browsers fetch the web app manifest in the background to
// evaluate PWA installability
- "/manifest.json", "/manifest.webmanifest" };
+ "/manifest.json", "/manifest.webmanifest",
+ // Legacy IE11/Edge fetch browserconfig.xml in the background to
+ // configure pinned-site tiles
+ "/browserconfig.xml" };
private static ServerWebExchangeMatcher createDefaultRequestMatcher() {
ServerWebExchangeMatcher get = ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/**");
diff --git a/web/src/test/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCacheTests.java b/web/src/test/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCacheTests.java
index 75624d58c7..efe80eb252 100644
--- a/web/src/test/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCacheTests.java
+++ b/web/src/test/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCacheTests.java
@@ -122,6 +122,15 @@ public class CookieServerRequestCacheTests {
assertThat(cookies).isEmpty();
}
+ @Test
+ public void saveRequestWhenGetRequestBrowserConfigXmlThenNoCookie() {
+ MockServerWebExchange exchange = MockServerWebExchange
+ .from(MockServerHttpRequest.get("/browserconfig.xml").accept(MediaType.TEXT_HTML));
+ this.cache.saveRequest(exchange).block();
+ MultiValueMap cookies = exchange.getResponse().getCookies();
+ assertThat(cookies).isEmpty();
+ }
+
@Test
public void saveRequestWhenPostRequestThenNoCookie() {
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.post("/secured/"));
diff --git a/web/src/test/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCacheTests.java b/web/src/test/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCacheTests.java
index 4b68546025..7acb99943a 100644
--- a/web/src/test/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCacheTests.java
+++ b/web/src/test/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCacheTests.java
@@ -113,6 +113,15 @@ public class WebSessionServerRequestCacheTests {
assertThat(saved).isNull();
}
+ @Test
+ public void saveRequestGetRequestWhenBrowserConfigXmlThenNotFound() {
+ MockServerWebExchange exchange = MockServerWebExchange
+ .from(MockServerHttpRequest.get("/browserconfig.xml").accept(MediaType.TEXT_HTML));
+ this.cache.saveRequest(exchange).block();
+ URI saved = this.cache.getRedirectUri(exchange).block();
+ assertThat(saved).isNull();
+ }
+
@Test
public void saveRequestGetRequestWhenPostThenNotFound() {
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.post("/secured/"));
From 0ca2c392447bcc25b1562c45effb62c31082cc74 Mon Sep 17 00:00:00 2001
From: Robert Winch <362503+rwinch@users.noreply.github.com>
Date: Wed, 9 Sep 2026 14:12:56 -0500
Subject: [PATCH 4/4] Ignore Chrome DevTools well-known probe in default
RequestCache
Chrome DevTools itself (not the loaded page, and not the user)
requests /.well-known/appspecific/com.chrome.devtools.json when
DevTools is open against a page, as part of the Automatic Workspace
Folders feature, to map a local project folder for live source
editing. This request is tooling-driven, not user navigation, so it
should never be able to become the URL a user is redirected to after
authenticating.
Closes gh-19694
---
.../web/configurers/RequestCacheConfigurer.java | 5 ++++-
.../configurers/RequestCacheConfigurerTests.java | 14 ++++++++++++++
.../savedrequest/CookieServerRequestCache.java | 5 ++++-
.../savedrequest/WebSessionServerRequestCache.java | 5 ++++-
.../CookieServerRequestCacheTests.java | 10 ++++++++++
.../WebSessionServerRequestCacheTests.java | 10 ++++++++++
6 files changed, 46 insertions(+), 3 deletions(-)
diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java
index 4002a3b9f4..da949c8830 100644
--- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java
+++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java
@@ -188,7 +188,10 @@ public final class RequestCacheConfigurer>
"/manifest.json", "/manifest.webmanifest",
// Legacy IE11/Edge fetch browserconfig.xml in the background to
// configure pinned-site tiles
- "/browserconfig.xml" };
+ "/browserconfig.xml",
+ // Chrome DevTools itself (not the page, not the user) requests this to
+ // discover an Automatic Workspace Folder mapping for local source editing
+ "/.well-known/appspecific/com.chrome.devtools.json" };
private RequestMatcher getIgnoredBackgroundRequestMatcher() {
List matchers = new ArrayList<>();
diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java
index 48c7304ea3..905bb7f99d 100644
--- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java
+++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java
@@ -200,6 +200,20 @@ public class RequestCacheConfigurerTests {
this.mvc.perform(formLogin(session)).andExpect(redirectedUrl("/"));
}
+ @Test
+ public void getWhenBookmarkedUrlIsChromeDevtoolsJsonThenPostAuthenticationRedirectsToRoot() throws Exception {
+ this.spring.register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class).autowire();
+ // @formatter:off
+ MockHttpSession session = (MockHttpSession) this.mvc.perform(get("/.well-known/appspecific/com.chrome.devtools.json"))
+ .andExpect(redirectedUrl("/login"))
+ .andReturn()
+ .getRequest()
+ .getSession();
+ // @formatter:on
+ // ignores /.well-known/appspecific/com.chrome.devtools.json
+ this.mvc.perform(formLogin(session)).andExpect(redirectedUrl("/"));
+ }
+
// SEC-2321
@Test
public void getWhenBookmarkedRequestIsApplicationJsonThenPostAuthenticationRedirectsToRoot() throws Exception {
diff --git a/web/src/main/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCache.java b/web/src/main/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCache.java
index 4361bf7dc3..e52a50ee49 100644
--- a/web/src/main/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCache.java
+++ b/web/src/main/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCache.java
@@ -174,7 +174,10 @@ public class CookieServerRequestCache implements ServerRequestCache {
"/manifest.json", "/manifest.webmanifest",
// Legacy IE11/Edge fetch browserconfig.xml in the background to
// configure pinned-site tiles
- "/browserconfig.xml" };
+ "/browserconfig.xml",
+ // Chrome DevTools itself (not the page, not the user) requests this to
+ // discover an Automatic Workspace Folder mapping for local source editing
+ "/.well-known/appspecific/com.chrome.devtools.json" };
private static ServerWebExchangeMatcher createDefaultRequestMatcher() {
ServerWebExchangeMatcher get = ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/**");
diff --git a/web/src/main/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCache.java b/web/src/main/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCache.java
index b3f72559ae..9944d2cb07 100644
--- a/web/src/main/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCache.java
+++ b/web/src/main/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCache.java
@@ -174,7 +174,10 @@ public class WebSessionServerRequestCache implements ServerRequestCache {
"/manifest.json", "/manifest.webmanifest",
// Legacy IE11/Edge fetch browserconfig.xml in the background to
// configure pinned-site tiles
- "/browserconfig.xml" };
+ "/browserconfig.xml",
+ // Chrome DevTools itself (not the page, not the user) requests this to
+ // discover an Automatic Workspace Folder mapping for local source editing
+ "/.well-known/appspecific/com.chrome.devtools.json" };
private static ServerWebExchangeMatcher createDefaultRequestMatcher() {
ServerWebExchangeMatcher get = ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/**");
diff --git a/web/src/test/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCacheTests.java b/web/src/test/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCacheTests.java
index efe80eb252..ea045d1acb 100644
--- a/web/src/test/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCacheTests.java
+++ b/web/src/test/java/org/springframework/security/web/server/savedrequest/CookieServerRequestCacheTests.java
@@ -131,6 +131,16 @@ public class CookieServerRequestCacheTests {
assertThat(cookies).isEmpty();
}
+ @Test
+ public void saveRequestWhenGetRequestChromeDevtoolsJsonThenNoCookie() {
+ MockServerWebExchange exchange = MockServerWebExchange
+ .from(MockServerHttpRequest.get("/.well-known/appspecific/com.chrome.devtools.json")
+ .accept(MediaType.TEXT_HTML));
+ this.cache.saveRequest(exchange).block();
+ MultiValueMap cookies = exchange.getResponse().getCookies();
+ assertThat(cookies).isEmpty();
+ }
+
@Test
public void saveRequestWhenPostRequestThenNoCookie() {
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.post("/secured/"));
diff --git a/web/src/test/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCacheTests.java b/web/src/test/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCacheTests.java
index 7acb99943a..d2b7c5446d 100644
--- a/web/src/test/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCacheTests.java
+++ b/web/src/test/java/org/springframework/security/web/server/savedrequest/WebSessionServerRequestCacheTests.java
@@ -122,6 +122,16 @@ public class WebSessionServerRequestCacheTests {
assertThat(saved).isNull();
}
+ @Test
+ public void saveRequestGetRequestWhenChromeDevtoolsJsonThenNotFound() {
+ MockServerWebExchange exchange = MockServerWebExchange
+ .from(MockServerHttpRequest.get("/.well-known/appspecific/com.chrome.devtools.json")
+ .accept(MediaType.TEXT_HTML));
+ this.cache.saveRequest(exchange).block();
+ URI saved = this.cache.getRedirectUri(exchange).block();
+ assertThat(saved).isNull();
+ }
+
@Test
public void saveRequestGetRequestWhenPostThenNotFound() {
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.post("/secured/"));