Self-contained project: https://github.com/McKeeAtx/starter-actuator-breaks-tests
Setup: A Spring Boot application with dependencies as follows:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web:3.0.2'
implementation 'org.springframework.boot:spring-boot-starter-security:3.0.2'
implementation 'org.springframework.boot:spring-boot-starter-actuator:3.0.2'
testImplementation 'org.springframework.boot:spring-boot-starter-test:3.0.2'
}
Application class:
@EnableMethodSecurity
@SpringBootApplication
public class Application {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authz) -> authz
.anyRequest().permitAll()
)
.httpBasic(withDefaults());
return http.build();
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
I have two tests that perform a GET request against an unknown endpoint:
- the first tests sends a valid header
- the second tests sends a header that is rejected by
org.springframework.security.web.firewall.StrictHttpFirewall
@Test
void testGetWithValidHeader() throws Exception {
var validHeader = "Bearer ABCD";
mockMvc
.perform(get("/unknown-endpoint").header("Authorization", validHeader))
.andExpect(status().isNotFound());
}
@Test
void testGetWithInvalidHeader() throws Exception {
var invalidHeader = "Bearer \t";
mockMvc
.perform(get("/unknown-endpoint").header("Authorization", invalidHeader))
.andExpect(status().is4xxClientError());
}
The first test passes. The 2nd test fails because the service returns an empty response with status code 200.
The 2nd test passes if I remove spring-boot-starter-actuator:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web:3.0.2'
implementation 'org.springframework.boot:spring-boot-starter-security:3.0.2'
// implementation 'org.springframework.boot:spring-boot-starter-actuator:3.0.2'
testImplementation 'org.springframework.boot:spring-boot-starter-test:3.0.2'
}
Is this expected behavior or a bug?
Comment From: McKeeAtx
A little bit of analysis:
spring-boot-starter-actuator registers a ObservationRegistry of type io.micrometer.observation.SimpleObservationRegistry. This causes org.springframework.security.config.annotation.web.builders.WebSecurity to set a RequestRejectedHandler of type org.springframework.security.web.firewall.ObservationMarkingRequestRejectedHandler :
...
else if (!this.observationRegistry.isNoop()) {
filterChainProxy
.setRequestRejectedHandler(new ObservationMarkingRequestRejectedHandler(this.observationRegistry));
}
....
ObservationMarkingRequestRejectedHandler creates a response with status code 200 if the client sends an invalid header that raises a RequestRejectedException in StrictHttpFirewall.
The RequestRejectedHandler of type HttpStatusRequestRejectedHandler that is used when spring-boot-starter-actuator is NOT on the classpath generates a status code in the 4xx range if the client sends an invalid header that raises a RequestRejectedException in StrictHttpFirewall. This is the behavior I would expect.
Comment From: wilkinsona
Thanks for the report. This is a duplicate of https://github.com/spring-projects/spring-security/issues/12548.
While we don't consider this to be security vulnerability, in the future please consider reporting security-related problems that have the potential to be a vulnerability by following the instructions in https://spring.io/security-policy.
Comment From: McKeeAtx
Gotcha - thanks for the update. I'll adhere to the security policy for anything that might be security related going forward.