1
0
mirror of synced 2026-09-12 12:15:13 +00:00

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 <link> 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
This commit is contained in:
Robert Winch
2026-09-09 13:54:54 -05:00
parent a7410ddc37
commit 242a779034
6 changed files with 149 additions and 10 deletions
@@ -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<H extends HttpSecurityBuilder<H>>
@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<H extends HttpSecurityBuilder<H>>
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<H extends HttpSecurityBuilder<H>>
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 <link> tag in the page
"/apple-touch-icon*.png" };
private RequestMatcher getIgnoredBackgroundRequestMatcher() {
List<RequestMatcher> matchers = new ArrayList<>();
for (String pattern : IGNORED_BACKGROUND_REQUEST_PATTERNS) {
matchers.add(getRequestMatcherBuilder().matcher(pattern));
}
return new OrRequestMatcher(matchers);
}
}
@@ -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 {
@@ -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 <link> 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);
}
}
@@ -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 <link> 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);
}
}
@@ -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<String, ResponseCookie> 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<String, ResponseCookie> 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<String, ResponseCookie> cookies = exchange.getResponse().getCookies();
assertThat(cookies).isEmpty();
}
@Test
public void saveRequestWhenPostRequestThenNoCookie() {
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.post("/secured/"));
@@ -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/"));