CSRF Protection in Spring Security: Complete Guide with Examples
Learn CSRF protection in Spring Security with production-tested examples, debugging tips, and real-world incident analysis.
20+ years shipping production Java in banking & fintech. Everything here is grounded in real deployments.
- ✓Java 17+
- ✓Spring Boot 3.x
- ✓Spring Security 6.x
- ✓Basic understanding of HTTP and cookies
- CSRF protection prevents malicious requests from authenticated users.
- Spring Security enables CSRF by default for state-changing operations (POST, PUT, DELETE).
- For REST APIs using stateless authentication (JWT), disable CSRF.
- For traditional web apps, use token-based protection with SameSite cookies.
- Always test CSRF with browser dev tools and security scanners.
Imagine you're logged into your bank. A bad guy sends you a link that, when clicked, transfers money out of your account without you knowing. CSRF protection is like a secret handshake that your browser and the bank agree on, so only real requests from you go through.
If you're building a web application with Spring Security, CSRF (Cross-Site Request Forgery) protection is one of those things that sounds simple but bites you in production. I've seen teams disable it because 'we have JWT, we're stateless' — and then wonder why their session-based admin panel gets hijacked. The truth is, CSRF protection isn't one-size-fits-all. In this guide, I'll show you exactly when to enable it, when to disable it, and how to configure it properly. We'll cover the default behavior in Spring Security 6.x, how to customize token generation, and what the official docs gloss over. By the end, you'll know how to protect your users without breaking your API.
What is CSRF and Why Should You Care?
Cross-Site Request Forgery (CSRF) is an attack that tricks an authenticated user into executing unwanted actions on a web application. Imagine a user logged into their bank. An attacker sends them a link to a malicious page that contains a hidden form submitting a transfer request. Since the user's browser automatically includes cookies (like session cookies), the bank server sees a legitimate request from an authenticated user. Spring Security's CSRF protection prevents this by requiring a unique token that the attacker cannot guess.
In Spring Security 6.x, CSRF protection is enabled by default for any request that modifies state (POST, PUT, DELETE, PATCH). For GET requests, it's typically not needed because they shouldn't change state. However, if you're building a pure REST API with stateless authentication (like JWT in Authorization header), you should disable CSRF because there's no session cookie to hijack. But — and this is a big but — if you store your JWT in a cookie, you still need CSRF protection.
Here's the default configuration in a Spring Boot 3.x app with Spring Security:
``java @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(``Customizer.withDefaults()) // enabled by default .authorizeHttpRequests(authz -> authz .anyRequest().authenticated() ) .formLogin(Customizer.withDefaults()); return http.build(); } }
This gives you a per-session CSRF token stored in the HttpSession. The token is exposed to the client via a cookie (by default named XSRF-TOKEN) or as a request attribute. The client must send this token back in a header (X-CSRF-TOKEN) or parameter (_csrf).
Configuring CSRF Protection: The Right Way
Spring Security provides several ways to configure CSRF protection. The most common is the default HttpSessionCsrfTokenRepository, but you can customize the token repository, the request matcher, and how the token is transmitted.
For traditional server-rendered applications (Thymeleaf, JSP), the default works fine. For Single Page Applications (SPAs) like Angular or React, you need to expose the token to JavaScript. That's where CookieCsrfTokenRepository comes in. It stores the token in a cookie (XSRF-TOKEN) that the client can read and send back.
Here's how to configure CookieCsrfTokenRepository:
``java http .csrf(csrf -> csrf .csrfTokenRepository(``CookieCsrfTokenRepository.withHttpOnlyFalse()) );
Note: withHttpOnlyFalse() is required so that JavaScript can read the cookie. This is a security trade-off — if your site has an XSS vulnerability, an attacker could steal the token. But for SPAs, it's necessary.
You can also restrict CSRF protection to specific patterns using CsrfConfigurer.requireCsrfProtectionMatcher. For example, you might want to exclude WebSocket endpoints:
``java http .csrf(csrf -> csrf .requireCsrfProtectionMatcher(new ``RequestMatcher() { private Pattern pattern = Pattern.compile("/websocket/.*"); @Override public boolean matches(HttpServletRequest request) { return !pattern.matcher(request.getRequestURI()).matches(); } }) );
But the most important configuration is knowing when to disable CSRF entirely. For REST APIs that use Bearer token authentication (JWT in Authorization header), disable CSRF:
``java http .csrf(csrf -> ``csrf.disable()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
I've seen teams blindly copy this without understanding why. If you disable CSRF but still use session cookies, you're vulnerable.
CSRF with Angular, React, and Other SPAs
Integrating CSRF protection with a frontend framework requires coordination. The backend must expose the token, and the frontend must include it in every state-changing request.
For Angular, the HttpClient module has built-in CSRF support. It looks for a cookie named XSRF-TOKEN and automatically sends it as a header (X-XSRF-TOKEN). This works seamlessly with CookieCsrfTokenRepository. However, there's a catch: Angular only sends the token for mutating requests (POST, PUT, DELETE, PATCH) and only to the same origin. That's perfect.
Here's the Angular side (no special code needed):
``typescript import { HttpClientModule } from '@angular/common/http'; // Angular automatically handles CSRF token ``
For React, you need to manually read the cookie and set the header. Use a library like js-cookie:
```javascript import Cookies from 'js-cookie';
const csrfToken = Cookies.get('XSRF-TOKEN'); fetch('/api/transfer', { method: 'POST', headers: { 'X-XSRF-TOKEN': csrfToken, 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ amount: 100 }) }); ```
A common pitfall: forgetting to set credentials: 'include' for cross-origin requests. Without it, the browser won't send cookies, and the CSRF token won't be available.
What the docs don't tell you: If you use a custom header name (e.g., X-CSRF-TOKEN instead of X-XSRF-TOKEN), you must configure Angular to expect it:
```typescript import { HttpClientXsrfModule } from '@angular/common/http';
@NgModule({ imports: [ HttpClientModule, HttpClientXsrfModule.withOptions({ cookieName: 'CSRF-TOKEN', headerName: 'X-CSRF-TOKEN' }) ] }) ```
What the Official Docs Won't Tell You
Here are the gotchas I've encountered in production that the Spring Security reference documentation either glosses over or omits entirely.
1. CSRF Token and Session Fixation The default CSRF token is stored in the session. If you change the session ID (e.g., after login), the old token becomes invalid. This is actually a security feature, but it means you must regenerate the token on login. Spring Security does this automatically, but if you're using a custom authentication filter, you might forget to clear the token repository.
2. Multiple Tabs Issue If a user opens multiple tabs, each tab gets the same session and thus the same CSRF token. That's fine — the token is per-session, not per-request. But if one tab logs out, the session is invalidated, and the other tabs will get 403 errors. This is expected behavior, but users will blame your app.
3. CSRF and File Uploads File uploads often use multipart/form-data. The CSRF token should be included as a form field, not in the URL. If you forget, the upload will fail with 403. Always include a hidden input for the token in your upload form.
4. Custom Header Names By default, Spring Security expects the token in a header named X-CSRF-TOKEN or a parameter named _csrf. If you change the header name on the client, you must also configure the server. Use CsrfTokenRequestAttributeHandler to customize.
5. Stateless APIs with Cookie-Based JWTs This is the biggest trap. You disable CSRF because you're stateless, but you store the JWT in a cookie. Now you're vulnerable again. The fix: either store the JWT in a header (Authorization: Bearer ...) or keep CSRF enabled.
6. Testing CSRF with MockMvc When writing tests, you need to include the CSRF token. Spring Security Test provides .with(csrf()) for MockMvc. If you forget, your POST tests will fail with 403.
7. CSRF and WebSockets WebSocket connections are not protected by CSRF by default because they use a different handshake. You need to implement custom protection, such as validating an origin header or using a token in the handshake request.
Advanced CSRF Configuration: Custom Token Repository and Stateless Tokens
For high-security applications, you might want to generate CSRF tokens without relying on sessions. This is useful for stateless architectures or when you want to avoid server-side state. You can implement a custom CsrfTokenRepository that generates tokens based on a secret key and user identity.
Here's a simple example of a stateless token repository using HMAC:
```java public class StatelessCsrfTokenRepository implements CsrfTokenRepository { private final String secret;
public StatelessCsrfTokenRepository(String secret) { this.secret = secret; }
@Override public CsrfToken generateToken(HttpServletRequest request) { String token = UUID.randomUUID().toString(); return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", token); }
@Override public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) { if (token == null) { // Remove token Cookie cookie = new Cookie("XSRF-TOKEN", ""); cookie.setMaxAge(0); response.addCookie(cookie); } else { // Store in cookie Cookie cookie = new Cookie("XSRF-TOKEN", token.getToken()); cookie.setHttpOnly(false); cookie.setSecure(true); cookie.setPath("/"); response.addCookie(cookie); } }
@Override public CsrfToken loadToken(HttpServletRequest request) { // Read from cookie Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if ("XSRF-TOKEN".equals(cookie.getName())) { return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", cookie.getValue()); } } } return null; } } ```
This is a simplified version. In production, you'd want to sign the token with a secret to prevent tampering. You can also use JWT as the CSRF token itself, embedding user identity and expiration.
Another advanced feature is to use a custom CsrfTokenRequestAttributeHandler to change how the token is resolved from the request. By default, Spring Security looks for the token in the header or parameter. You can override this to, say, validate a signature.
Testing CSRF Protection
Testing CSRF protection is critical, yet often overlooked. You need to ensure that requests without a valid token are rejected, and that legitimate requests succeed.
Spring Security Test provides a convenient .with(csrf()) method for MockMvc tests. Here's an example:
```java @SpringBootTest @AutoConfigureMockMvc public class CsrfTest { @Autowired private MockMvc mockMvc;
@Test public void testPostWithoutCsrf() throws Exception { mockMvc.perform(post("/api/transfer") .param("amount", "100")) .andExpect(status().isForbidden()); }
@Test public void testPostWithCsrf() throws Exception { mockMvc.perform(post("/api/transfer") .with(csrf()) .param("amount", "100")) .andExpect(status().isOk()); } } ```
For integration tests with a real browser, use tools like Selenium or Playwright. You can also use security scanners like OWASP ZAP to automate CSRF testing.
A common mistake: forgetting to include the CSRF token in WebClient or RestTemplate calls. If you're making server-side requests to another service that requires CSRF, you must pass the token. But in a microservices architecture, you typically disable CSRF for internal APIs.
What the docs don't tell you: MockMvc's .with(csrf()) only works if CSRF protection is enabled. If you've disabled it globally, the test will pass even without the token. Always write both positive and negative tests.
csrf()) for unit tests. Always test both valid and invalid CSRF scenarios.CSRF and CORS: The Dynamic Duo
CSRF and CORS are often confused but are different security mechanisms. CORS controls which origins can access your resources, while CSRF prevents unauthorized requests from authenticated users. They work together: CORS allows or blocks cross-origin requests, and CSRF ensures that allowed requests are intentional.
If you have a frontend on a different origin (e.g., React on localhost:3000, backend on localhost:8080), you need both CORS and CSRF configuration.
For CORS, Spring Security provides a CorsFilter:
``java @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new ``CorsConfiguration(); configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000")); configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE")); configuration.setAllowCredentials(true); configuration.setAllowedHeaders(Arrays.asList("")); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/*", configuration); return source; }
Then enable CORS in the filter chain:
``java http .cors(``Customizer.withDefaults()) .csrf(csrf -> csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) );
A critical gotcha: CORS preflight requests (OPTIONS) must not require CSRF tokens. Spring Security handles this automatically by not applying CSRF to OPTIONS requests, but if you have a custom filter, ensure it doesn't block preflight.
Another gotcha: If you set allowedOrigins to "*" with allowCredentials=true, the browser will reject the request. CORS spec doesn't allow wildcard origins with credentials. Always specify explicit origins.
The Fintech Payment Double-Charge Incident
- Never disable CSRF globally without understanding the risk.
- JWT in cookies is still vulnerable to CSRF without proper token validation.
- Use idempotency keys for critical operations like payments.
- Always test with a security scanner like OWASP ZAP.
- SameSite cookie attribute is not a silver bullet; combine with CSRF tokens.
curl -v -X POST -H 'X-CSRF-TOKEN: <token>' http://localhost:8080/api/endpointgrep -i csrf /var/log/spring/app.log| File | Command / Code | Purpose |
|---|---|---|
| SecurityConfig.java | @Configuration | What is CSRF and Why Should You Care? |
| CsrfConfigExample.java | @Configuration | Configuring CSRF Protection |
| CsrfAngularExample.java | http | CSRF with Angular, React, and Other SPAs |
| CsrfTestExample.java | @Test | What the Official Docs Won't Tell You |
| StatelessCsrfTokenRepository.java | public class StatelessCsrfTokenRepository implements CsrfTokenRepository { | Advanced CSRF Configuration |
| CsrfTest.java | @SpringBootTest | Testing CSRF Protection |
| CorsCsrfConfig.java | @Configuration | CSRF and CORS |
Key takeaways
Interview Questions on This Topic
How does Spring Security's CSRF protection work under the hood?
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Everything here is grounded in real deployments.
That's Spring Security. Mark it forged?
7 min read · try the examples if you haven't