Spring Boot Security’s CSRF and CORS are often configured together, but they solve fundamentally different problems, and their interaction can be a source of confusion.
Let’s see how they work together in a real scenario. Imagine a simple Spring Boot application with a REST controller and Spring Security enabled.
@SpringBootApplication
@RestController
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@GetMapping("/api/data")
public String getData() {
return "Some sensitive data";
}
}
Now, let’s add Spring Security with default CSRF protection and enable CORS for requests originating from http://localhost:3000 (a common frontend development server).
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // Temporarily disable CSRF for easier CORS demo
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/**").authenticated()
.anyRequest().permitAll()
)
.formLogin(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults());
return http.build();
}
// CORS configuration
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*");
}
};
}
}
In this setup, http.csrf(csrf -> csrf.disable()) is crucial for now. We’ll re-enable it later. The corsConfigurer bean sets up CORS. When a request comes into /api/** from http://localhost:3000, Spring Security will allow it, provided the origin header matches. If we were to try and access /api/data from a different origin (e.g., directly from http://localhost:8080 without CORS configured for it), the browser would block the request before it even reaches Spring Security’s CORS handling, showing a CORS error in the browser console.
CSRF, on the other hand, is about preventing malicious websites from tricking a logged-in user into performing unwanted actions on your web application. It assumes the request is coming from a legitimate user of your application, but from a different, untrusted origin.
Let’s re-enable CSRF and see how it affects things.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())) // Re-enable CSRF
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/**").authenticated()
.anyRequest().permitAll()
)
.formLogin(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults());
return http.build();
}
// CORS configuration (same as before)
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*");
}
};
}
}
When CSRF is enabled, Spring Security expects a CSRF token to be present in most state-changing requests (POST, PUT, DELETE). By default, it looks for this token in a request header named X-XSRF-TOKEN. The CookieCsrfTokenRepository.withHttpOnlyFalse() configuration tells Spring Security to set the CSRF token in a cookie, and importantly, makes that cookie accessible to JavaScript (i.e., HttpOnly=false). This is a common pattern for SPAs (Single Page Applications) where the frontend JavaScript reads the CSRF token from a cookie and then includes it in the X-XSRF-TOKEN header for subsequent requests.
If you were to send a POST request to an endpoint protected by CSRF without the X-XSRF-TOKEN header, you would get a 403 Forbidden error with the message "Invalid CSRF Token." This error is not a CORS error; it’s Spring Security’s CSRF protection kicking in.
The key distinction is that CORS is about browser-level restrictions on cross-origin requests, enforced by the browser itself to prevent a website from making requests to a different domain without explicit permission. CSRF, on the other hand, is an application-level security mechanism implemented by your server-side code to protect against forged requests from a user who is already authenticated.
When you have both CORS and CSRF enabled, the request flow typically looks like this:
- Browser: A request is initiated from
http://localhost:3000tohttp://localhost:8080/api/data. - Browser (CORS Preflight): For methods other than simple GET, POST, HEAD, the browser may send an
OPTIONSpreflight request tohttp://localhost:8080/api/data. - Spring Security (CORS): Spring Security’s CORS configuration checks the
Originheader (http://localhost:3000). If it’s allowed, it responds to the preflight request with appropriate CORS headers (e.g.,Access-Control-Allow-Origin). - Browser: If the preflight is successful (or if it wasn’t needed), the browser sends the actual request.
- Spring Security (CSRF): Spring Security’s CSRF filter inspects the request. It looks for the
X-XSRF-TOKENheader. If the request is state-changing and the token is missing or invalid, a403 Forbidden(CSRF error) is returned. If the token is present and valid, the request proceeds to the controller.
The most surprising true thing about this interaction is that CORS doesn’t inherently protect against CSRF. A malicious site can still send a request to your domain if the browser allows it via CORS, and if that request carries valid CSRF credentials (like a cookie that was automatically sent), your application might think it’s a legitimate request. This is why you need both.
One subtle point often missed is how the CSRF token is managed in a typical SPA. The CookieCsrfTokenRepository.withHttpOnlyFalse() makes the token readable by JavaScript. Your frontend JavaScript code must then:
- On initial page load, make a request (often to a dedicated
/csrfendpoint or by observing aGETrequest to a protected resource) that will cause Spring Security to set the CSRF token cookie. - Read this cookie value using
document.cookie. - For every subsequent state-changing request (POST, PUT, DELETE, etc.), add a header named
X-XSRF-TOKENwith the value read from the cookie.
Without this client-side logic, enabling CSRF protection will break all your state-changing requests originating from your frontend, even if CORS is perfectly configured.
The next concept you’ll likely encounter is how to handle CSRF and CORS with different request types or for API endpoints that don’t use cookies for session management, such as stateless JWT authentication.