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 Forbidden on POST/PUT/DELETE requests
  • Invalid 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 Denied error 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. 1.Include CSRF token in HTML forms:
  2. 2.```html
  3. 3.<!-- Spring Security automatically adds this with Thymeleaf -->
  4. 4.<form method="post" action="/api/users">
  5. 5.<input type="hidden"
  6. 6.name="_csrf"
  7. 7.value="${_csrf.token}"/>
  8. 8.<input type="hidden"
  9. 9.name="_csrf_header"
  10. 10.value="${_csrf.headerName}"/>
  11. 11.<input type="text" name="name"/>
  12. 12.<button type="submit">Submit</button>
  13. 13.</form>
  14. 14.`
  15. 15.Include CSRF token in JavaScript AJAX requests:
  16. 16.```javascript
  17. 17.// Read token from meta tag
  18. 18.const csrfToken = document.querySelector('meta[name="_csrf"]').content;
  19. 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. 1.Expose CSRF token via endpoint for SPAs:
  2. 2.```java
  3. 3.@Configuration
  4. 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. 1.Disable CSRF for stateless APIs:
  2. 2.```java
  3. 3.@Configuration
  4. 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. 1.Configure custom CSRF token header:
  2. 2.```java
  3. 3.@Bean
  4. 4.public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  5. 5.http.csrf(csrf -> csrf
  6. 6..csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
  7. 7.);
  8. 8.return http.build();
  9. 9.}
  10. 10.// Default: reads from cookie 'XSRF-TOKEN', expects header 'X-XSRF-TOKEN'
  11. 11.// Compatible with Angular's built-in CSRF protection
  12. 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 SameSite cookie attribute as additional CSRF defense layer