From 564b65bc789a34f027ee2465b8fe3b16486b0105 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 9 Jun 2025 10:45:52 +0100 Subject: [PATCH] Permit write and delete operations on Cloud Foundry with Spring Security Update `CloudFoundryActuatorAutoConfiguration` so that CSRF protection is ignored for Cloud Foundry endpoint requests. This aligns with the behavior of Spring Boot 3.4 where `ignoring()` was used for security configuration. With the migration to `permitAll()` we now need additional CSRF configuration. Fixes gh-45848 --- ...CloudFoundryActuatorAutoConfiguration.java | 1 + ...FoundryActuatorAutoConfigurationTests.java | 29 +++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) 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 fc9bbf7ee7..4a6bc50c6e 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 @@ -174,6 +174,7 @@ public class CloudFoundryActuatorAutoConfiguration { SecurityFilterChain cloudFoundrySecurityFilterChain(HttpSecurity http, CloudFoundryWebEndpointServletHandlerMapping handlerMapping) throws Exception { RequestMatcher cloudFoundryRequest = getRequestMatcher(handlerMapping); + http.csrf((csrf) -> csrf.ignoringRequestMatchers(cloudFoundryRequest)); 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 90c0c43998..aa9d5d8d00 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 @@ -33,6 +33,7 @@ import org.springframework.boot.actuate.endpoint.ApiVersion; import org.springframework.boot.actuate.endpoint.EndpointId; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.annotation.WriteOperation; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; import org.springframework.boot.actuate.endpoint.web.WebOperation; @@ -49,17 +50,24 @@ import org.springframework.boot.test.context.assertj.AssertableWebApplicationCon import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.context.ApplicationContext; import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.security.config.BeanIds; import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.MockMvc; 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.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.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** * Tests for {@link CloudFoundryActuatorAutoConfiguration}. @@ -170,7 +178,7 @@ class CloudFoundryActuatorAutoConfigurationTests { } @Test - void cloudFoundryPathsIgnoredBySpringSecurity() { + void cloudFoundryPathsPermittedBySpringSecurity() { this.contextRunner.withBean(TestEndpoint.class, TestEndpoint::new) .withPropertyValues("VCAP_APPLICATION:---", "vcap.application.application_id:my-app-id") .run((context) -> { @@ -189,6 +197,19 @@ class CloudFoundryActuatorAutoConfigurationTests { }); } + @Test + void cloudFoundryPathsPermittedWithCsrfBySpringSecurity() { + this.contextRunner.withBean(TestEndpoint.class, TestEndpoint::new) + .withPropertyValues("VCAP_APPLICATION:---", "vcap.application.application_id:my-app-id") + .run((context) -> { + MockMvc mvc = MockMvcBuilders.webAppContextSetup(context).apply(springSecurity()).build(); + mvc.perform(post(BASE_PATH + "/test?name=test").contentType(MediaType.APPLICATION_JSON) + .with(csrf().useInvalidToken())).andExpect(status().isServiceUnavailable()); + // If CSRF 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); @@ -258,7 +279,7 @@ class CloudFoundryActuatorAutoConfigurationTests { .findFirst() .get(); Collection operations = endpoint.getOperations(); - assertThat(operations).hasSize(1); + assertThat(operations).hasSize(2); assertThat(operations.iterator().next().getRequestPredicate().getPath()).isEqualTo("test"); }); } @@ -307,6 +328,10 @@ class CloudFoundryActuatorAutoConfigurationTests { return "hello world"; } + @WriteOperation + void update(String name) { + } + } }