Introduction

Spring Security enables CSRF protection by default, requiring all state-changing HTTP requests (POST, PUT, DELETE, PATCH) to include a valid CSRF token. When the token is missing, expired, or mismatched, Spring returns HTTP 403 Forbidden with Invalid CSRF Token message. This commonly affects SPA frontends that do not include the token in AJAX requests, APIs that use stateless JWT authentication where CSRF is not needed, and forms rendered by server-side templates that do not properly include the token. Understanding when CSRF is needed and how to configure it correctly is essential for secure web applications.

Symptoms

json
{
    "timestamp": "2026-04-09T10:00:00.000+00:00",
    "status": 403,
    "error": "Forbidden",
    "message": "Invalid CSRF Token 'null' was found on the request parameter '_csrf' or header 'X-CSRF-TOKEN'."
}

Or:

bash
org.springframework.security.web.csrf.InvalidCsrfTokenException: Invalid CSRF Token

Common Causes

  • Token not included in AJAX request: Frontend does not send X-CSRF-TOKEN header
  • Token expired: Session expired, old token no longer valid
  • CSRF enabled for stateless API: REST API with JWT does not need CSRF
  • Token not in HTML form: Thymeleaf form missing CSRF token
  • Cookie-based CSRF not configured: CsrfToken not written to cookie
  • Multiple tabs sharing same token: Token invalidated by one tab affects others

Step-by-Step Fix

Step 1: Configure CSRF token cookie for SPA frontends

```java @Configuration @EnableWebSecurity public class SecurityConfig {

@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // Token written to cookie, readable by JavaScript .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()) ); return http.build(); } } ```

Step 2: Include CSRF token in AJAX requests

```javascript // Read token from cookie (set by Spring Security) function getCsrfToken() { const name = 'XSRF-TOKEN'; const cookies = document.cookie.split(';'); for (let cookie of cookies) { const [key, value] = cookie.trim().split('='); if (key === name) return decodeURIComponent(value); } return null; }

// Include in fetch requests async function apiCall(url, options = {}) { const token = getCsrfToken(); return fetch(url, { ...options, headers: { ...options.headers, 'X-CSRF-TOKEN': token, 'Content-Type': 'application/json', }, }); }

// Or with Axios axios.defaults.headers.common['X-CSRF-TOKEN'] = getCsrfToken(); ```

Step 3: Disable CSRF for stateless APIs

```java @Configuration @EnableWebSecurity public class ApiSecurityConfig {

@Bean public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception { http .securityMatcher("/api/**") .csrf(csrf -> csrf.disable()) // Disable for stateless API .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/public/**").permitAll() .anyRequest().authenticated() ); return http.build(); } } ```

Prevention

  • Use CookieCsrfTokenRepository.withHttpOnlyFalse() for SPA + Spring Security
  • Include X-CSRF-TOKEN header in all state-changing AJAX requests
  • Disable CSRF only for truly stateless APIs (JWT/OAuth), never for session-based apps
  • Add CSRF token to HTML forms with <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
  • Use Thymeleaf's automatic CSRF inclusion with th:action on forms
  • Test CSRF protection by making requests without tokens to verify they are rejected
  • Monitor 403 CSRF errors in production to detect frontend integration issues