Spring Boot Tutorial
CSRF and CORS in Spring Security
CSRF and CORS are two of the most misunderstood settings in web security, and they are often "fixed" by disabling protections until an error goes away. Both exist because of how browsers automatically send cookies and enforce the same-origin policy.
This lesson explains what cross-site request forgery actually is, when CSRF protection is needed and when it is safe to turn off, how to make CSRF work with single-page applications, and how to configure CORS correctly so your React or Angular front end can call your Spring Boot API without opening it to every website on the internet.
What CSRF Attacks Exploit
Browsers attach cookies to every request to a site, no matter which page started the request. If a user is logged into bank.example with a session cookie and then visits a malicious page, that page can submit a hidden form to bank.example/transfer and the browser will include the session cookie. The bank sees a perfectly authenticated request the user never intended.
CSRF protection defends against this by requiring every state-changing request (POST, PUT, PATCH, DELETE) to include a secret token that the attacker's page cannot read. Spring Security enables this by default.
When You Need CSRF Protection
The rule is simple: if the browser authenticates requests automatically — session cookies, remember-me cookies, HTTP Basic cached by the browser — you need CSRF protection. If every request carries a token that JavaScript must add explicitly, such as Authorization: Bearer, an attacker's page cannot forge it and CSRF protection can be disabled for those endpoints.
Storing a JWT in a cookie brings the CSRF risk straight back, because the browser sends that cookie automatically.
CSRF with Single-Page Applications
A React app that uses session cookies needs to read the CSRF token and send it back in a header. Spring Security 6.3+ provides csrf.spa(), which stores the token in a readable XSRF-TOKEN cookie and accepts it in the X-XSRF-TOKEN header (the convention Axios and Angular already follow). Traditional server-rendered pages with Thymeleaf get the token inserted into forms automatically.
What CORS Is (and Is Not)
The same-origin policy stops JavaScript on https://app.webnest.in from reading responses from https://api.webnest.in, because they are different origins. CORS (Cross-Origin Resource Sharing) is how the API tells the browser which other origins are allowed to read its responses. For non-simple requests (JSON bodies, custom headers, PUT/DELETE) the browser first sends an OPTIONS preflight request.
CORS is a browser feature, not an access-control mechanism. curl, Postman and server-to-server calls ignore it completely. It never replaces authentication or authorization.
Configuring CORS in Spring Security
Because preflight requests carry no credentials, CORS must be handled before authentication; otherwise the preflight gets a 401 and the browser blocks the real request. Enable it with http.cors(Customizer.withDefaults()), which picks up a CorsConfigurationSource bean. List exact origins; never combine allowCredentials(true) with a wildcard origin.
Examples
Default CSRF protection with a Thymeleaf form
<!-- Thymeleaf adds the hidden _csrf field automatically to th:action forms -->
<form th:action="@{/account/email}" method="post">
<input type="email" name="email">
<button type="submit">Update email</button>
</form>
<!-- Rendered HTML -->
<form action="/account/email" method="post">
<input type="hidden" name="_csrf" value="f8e7c3a1-2b4d-4e6f-9a0b-1c2d3e4f5a6b"/>
<input type="email" name="email">
<button type="submit">Update email</button>
</form>
POST /account/email with valid _csrf -> 302 /account (updated)
POST /account/email from evil.example page -> 403 Forbidden (Invalid CSRF token)
CSRF for a React SPA using session cookies
@Bean
SecurityFilterChain spa(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
.csrf(csrf -> csrf.spa()); // XSRF-TOKEN cookie + X-XSRF-TOKEN header
return http.build();
}
// React side: axios reads the XSRF-TOKEN cookie and sends X-XSRF-TOKEN automatically
// axios.defaults.withCredentials = true;
// await axios.post('/api/profile', { displayName: 'Asha' });
Response header: Set-Cookie: XSRF-TOKEN=6c1f...; Path=/
Next request: X-XSRF-TOKEN: 6c1f... -> 200 OK
Stateless bearer-token API: CSRF off, CORS on for a known front end
@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
http
.cors(Customizer.withDefaults()) // uses the CorsConfigurationSource bean
.csrf(csrf -> csrf.disable()) // safe: auth is an explicit Bearer header, not a cookie
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()));
return http.build();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://app.webnest.in", "http://localhost:5173"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
config.setExposedHeaders(List.of("Location"));
config.setMaxAge(Duration.ofHours(1));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
OPTIONS /api/orders Origin: https://app.webnest.in
-> 200, Access-Control-Allow-Origin: https://app.webnest.in, Access-Control-Allow-Methods: GET,POST,PUT,PATCH,DELETE,OPTIONS
OPTIONS /api/orders Origin: https://evil.example
-> 403 Invalid CORS request
Common Mistakes
- Disabling CSRF on an application that authenticates with session cookies, just to make a POST from Postman work.
- Storing a JWT in a cookie and disabling CSRF because "we use JWT" — cookie-borne tokens are sent automatically and are vulnerable to CSRF.
- Using allowedOrigins("*") together with allowCredentials(true); browsers reject it, and allowedOriginPatterns("*") with credentials exposes your API to every site.
- Adding @CrossOrigin on controllers but not http.cors(...), so preflight requests are rejected with 401 by Spring Security before reaching MVC.
- Treating CORS as security — any non-browser client can still call the API; authentication and authorization must still be enforced.
Key Points to Remember
- CSRF protection is needed whenever the browser sends credentials automatically (session or auth cookies).
- Stateless APIs that require an explicit Authorization header can safely disable CSRF.
- csrf.spa() makes CSRF work with React, Angular and Axios using the XSRF-TOKEN cookie and header.
- CORS tells browsers which origins may read responses; configure it with http.cors() and a CorsConfigurationSource.
- List exact allowed origins and never rely on CORS for access control.
Practice the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Use your local JDK or project IDE for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.