Introduction
Spring Security enables CSRF (Cross-Site Request Forgery) protection by default. Every state-changing request (POST, PUT, DELETE, PATCH) must include a valid CSRF token. If the token is missing, expired, or does not match the session, Spring Security returns 403 Forbidden. This is a security feature, but it commonly breaks AJAX requests, mobile app APIs, and single-page applications that are not configured to send the token.
Symptoms
HTTP 403 Forbiddenon POST/PUT/DELETE requestsInvalid CSRF Token 'null' was found on the request parameter '_csrf' or header 'X-CSRF-TOKEN'- Requests work in Postman but fail from browser JavaScript
Access Deniederror in Spring Security logs- Works for GET requests but fails for POST
``` 2024-01-15 10:30:00.123 WARN --- [nio-8080-exec-5] o.s.security.web.csrf.CsrfFilter : Invalid CSRF token found for http://localhost:8080/api/users
HTTP/1.1 403 Forbidden Content-Type: application/json { "timestamp": "2024-01-15T10:30:00.123+00:00", "status": 403, "error": "Forbidden", "message": "Invalid CSRF Token", "path": "/api/users" } ```
Common Causes
- Frontend not sending CSRF token in AJAX requests
- Token expired due to session timeout
- Token sent in wrong header name
- API clients (mobile apps, microservices) not designed for CSRF
- CORS preflight requests not handled correctly with CSRF
Step-by-Step Fix
- 1.Include CSRF token in HTML forms:
- 2.```html
- 3.<!-- Spring Security automatically adds this with Thymeleaf -->
- 4.<form method="post" action="/api/users">
- 5.<input type="hidden"
- 6.name="_csrf"
- 7.value="${_csrf.token}"/>
- 8.<input type="hidden"
- 9.name="_csrf_header"
- 10.value="${_csrf.headerName}"/>
- 11.<input type="text" name="name"/>
- 12.<button type="submit">Submit</button>
- 13.</form>
- 14.
` - 15.Include CSRF token in JavaScript AJAX requests:
- 16.```javascript
- 17.// Read token from meta tag
- 18.const csrfToken = document.querySelector('meta[name="_csrf"]').content;
- 19.const csrfHeader = document.querySelector('meta[name="_csrf_header"]').content;
// Include in fetch request fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json', [csrfHeader]: csrfToken // Key: X-CSRF-TOKEN }, body: JSON.stringify({ name: 'John' }) });
// Or with Axios axios.defaults.headers.common[csrfHeader] = csrfToken; ```
- 1.Expose CSRF token via endpoint for SPAs:
- 2.```java
- 3.@Configuration
- 4.public class SecurityConfig {
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler()) ); return http.build(); } }
// SPA reads token from cookie named 'XSRF-TOKEN' // And sends it back in 'X-XSRF-TOKEN' header ```
- 1.Disable CSRF for stateless APIs:
- 2.```java
- 3.@Configuration
- 4.public class SecurityConfig {
@Bean public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception { http .securityMatcher("/api/**") .csrf(csrf -> csrf.disable()) // Disable for REST API .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/**").authenticated() ); return http.build(); } } ```
- 1.Configure custom CSRF token header:
- 2.```java
- 3.@Bean
- 4.public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
- 5.http.csrf(csrf -> csrf
- 6..csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
- 7.);
- 8.return http.build();
- 9.}
- 10.// Default: reads from cookie 'XSRF-TOKEN', expects header 'X-XSRF-TOKEN'
- 11.// Compatible with Angular's built-in CSRF protection
- 12.
`
Prevention
- Use
CookieCsrfTokenRepository.withHttpOnlyFalse()for SPA integration - Disable CSRF only for truly stateless APIs with JWT authentication
- Keep CSRF enabled for session-based web applications
- Include CSRF token in all non-GET requests from the frontend
- Test CSRF protection in CI with integration tests
- Document the expected CSRF header name in API documentation
- Use
SameSitecookie attribute as additional CSRF defense layer