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/"));