From d2b62bc64f6a1e45e2f65293608c018a96e58971 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 30 Apr 2026 12:11:21 +0100 Subject: [PATCH] Fix CORS configuration in /cloudfoundryapplication security filter Previously, Spring Security wouldn't necessary allow cross-origin requests to /cloudfoundryapplication, despite the underlying handler mapping doing so. Since 6.2, Spring Security enables CORS with default configuration if there are any UrlBasedCorsConfigurationSource beans in the context. This default configuration will then use a bean named corsConfigurationSource as its source of CORS configuration. If it doesn't find such a bean, it'll use the mvcHandlerMappingIntrospector bean as a source. This latter case works as it means that the CORS configuration of the underlying handler mapping is used. In the case where a bean named corsConfigurationSource is used, this will break /cloudfoundryapplication if the bean's CORS configuration for /cloudfoundryapplication does not allow it. This has only been a problem since Spring Boot 3.5 where we switched from using ignoring() to using permitAll() to configure the security of /cloudfoundryapplication. To avoid a user-provided bean named corsConfigurationSource from accidentally preventing access to /cloudfoundryapplication, we now explicitly configure the filter chain's CORS support to use the same CorsConfigurationSource as the handler mapping. Fixes gh-50254 --- ...CloudFoundryActuatorAutoConfiguration.java | 4 +++- ...FoundryActuatorAutoConfigurationTests.java | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration.java index 0b2ce6c0c5a..d623abcf6ae 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration.java @@ -148,7 +148,7 @@ public class CloudFoundryActuatorAutoConfiguration { ? new CloudFoundrySecurityService(restTemplateBuilder, cloudControllerUrl, skipSslValidation) : null; } - private CorsConfiguration getCorsConfiguration() { + private static CorsConfiguration getCorsConfiguration() { CorsConfiguration corsConfiguration = new CorsConfiguration(); corsConfiguration.addAllowedOrigin(CorsConfiguration.ALL); corsConfiguration.setAllowedMethods(Arrays.asList(HttpMethod.GET.name(), HttpMethod.POST.name())); @@ -173,6 +173,8 @@ public class CloudFoundryActuatorAutoConfiguration { SecurityFilterChain cloudFoundrySecurityFilterChain(HttpSecurity http) throws Exception { RequestMatcher cloudFoundryRequest = getRequestMatcher(); http.csrf((csrf) -> csrf.ignoringRequestMatchers(cloudFoundryRequest)); + CorsConfiguration corsConfiguration = getCorsConfiguration(); + http.cors((cors) -> cors.configurationSource((request) -> corsConfiguration)); http.securityMatchers((matches) -> matches.requestMatchers(cloudFoundryRequest)) .authorizeHttpRequests((authorize) -> authorize.anyRequest().permitAll()); return http.build(); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfigurationTests.java index 39e90f0daa2..0c991c8aad4 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfigurationTests.java @@ -49,6 +49,7 @@ import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguratio import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; @@ -61,11 +62,13 @@ import org.springframework.test.web.servlet.assertj.MockMvcTester; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.client.RestTemplate; import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CompositeFilter; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -208,6 +211,22 @@ class CloudFoundryActuatorAutoConfigurationTests { }); } + @Test + void crossOriginRequestToCloudFoundryPathsPermittedBySpringSecurity() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", new CorsConfiguration()); + this.contextRunner.withBean(TestEndpoint.class, TestEndpoint::new) + .withBean("corsConfigurationSource", UrlBasedCorsConfigurationSource.class, () -> source) + .withPropertyValues("VCAP_APPLICATION:---", "vcap.application.application_id:my-app-id") + .run((context) -> { + MockMvc mvc = MockMvcBuilders.webAppContextSetup(context).apply(springSecurity()).build(); + mvc.perform(get(BASE_PATH + "/test").header(HttpHeaders.ORIGIN, "elsewhere.example.com") + .contentType(MediaType.APPLICATION_JSON)).andExpect(status().isServiceUnavailable()); + // If CORS fails we'll get a 403, if it works we get service unavailable + // because of "Cloud controller URL is not available" + }); + } + private SecurityFilterChain getSecurityFilterChain(AssertableWebApplicationContext context) { Filter springSecurityFilterChain = context.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN, Filter.class); FilterChainProxy filterChainProxy = getFilterChainProxy(springSecurityFilterChain);