Spring BootCSRF Protection
Spring Boot

CSRF Protection

CSRF (Cross-Site Request Forgery) is an attack where a malicious site tricks a user's browser into making an authenticated request to your application. Spring Security enables CSRF protection by default for all state-changing requests. Understanding when to enable it, when to disable it, and how to integrate it with Thymeleaf, REST APIs, and SPAs is essential for building secure applications.

What CSRF Is and When It Matters

A CSRF attack exploits the fact that browsers automatically include cookies (including session cookies) with every request to a domain. An attacker can embed a form or JavaScript on their site that submits a request to your application — and the browser silently attaches the victim's session cookie, making the request appear authenticated. CSRF protection is only necessary when your application uses cookie-based session authentication. For stateless APIs that use JWT bearer tokens in the Authorization header, CSRF protection is not needed — browsers never auto-attach Authorization headers across origins.
Shell
# ── CSRF is relevant when: ───────────────────────────────────────────
# - Authentication is stored in a cookie (JSESSIONID, remember-me cookie)
# - The application accepts form submissions or state-changing requests
# - You have a server-side rendered app with form login

# ── CSRF is NOT needed when: ─────────────────────────────────────────
# - Authentication uses a custom header (Authorization: Bearer <token>)
# - The API is consumed by non-browser clients (mobile, server-to-server)
# - The application is stateless (SessionCreationPolicy.STATELESS)

# ── The CSRF attack scenario: ─────────────────────────────────────────
# 1. User logs in to bank.com → session cookie JSESSIONID stored in browser
# 2. User visits evil.com (while still logged in)
# 3. evil.com contains: <form action="https://bank.com/transfer" method="POST">
#                         <input name="amount" value="10000">
#                         <input name="to" value="attacker">
#                       </form> + auto-submit JavaScript
# 4. Browser submits form to bank.com WITH the session cookie automatically
# 5. bank.com sees authenticated request → transfers money

# ── CSRF defence — synchronizer token pattern: ────────────────────────
# Server generates a unique token per session and embeds it in forms.
# On POST, server validates the token is present and correct.
# evil.com cannot read the CSRF token (same-origin policy) → attack fails.

Default CSRF Configuration

Spring Security enables CSRF protection by default. The CsrfFilter intercepts every non-safe request (POST, PUT, PATCH, DELETE) and validates the CSRF token. Safe methods (GET, HEAD, OPTIONS, TRACE) are not checked.
Java
// ── Default behaviour (no configuration needed): ────────────────────
// CsrfFilter is active for all non-safe HTTP methods.
// CSRF token stored in HttpSession by default.
// Token must be present in request as:
//   - Request parameter: _csrf
//   - Request header:    X-CSRF-TOKEN

// ── Spring Security's default CSRF config (what auto-configuration does):
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        // CSRF is ON by defaultthis is equivalent to the default:
        .csrf(csrf -> csrf
            .csrfTokenRepository(HttpSessionCsrfTokenRepository())
        );
    return http.build();
}

// ── Request without CSRF token → 403 Forbidden: ──────────────────────
// POST /users HTTP/1.1
// Content-Type: application/json
// Cookie: JSESSIONID=abc123
//
// { "name": "Alice" }
//
// Response: HTTP/1.1 403 Forbidden
// {"error": "Forbidden", "message": "Invalid CSRF token"}

// ── Request with valid CSRF token → succeeds: ────────────────────────
// POST /users HTTP/1.1
// Content-Type: application/json
// Cookie: JSESSIONID=abc123
// X-CSRF-TOKEN: 8f1e2a4c-9b3d-4e7f-a1c2-5d8e9f0b1c2d
//
// { "name": "Alice" }
//
// Response: HTTP/1.1 201 Created

CSRF with Thymeleaf Forms

Thymeleaf automatically includes the CSRF token in forms that use th:action. The hidden input is injected transparently — no manual configuration needed.
html
<!-- Thymeleaf form — CSRF token injected automatically by th:action: -->
<form th:action="@{/users}" method="post">
    <!-- Thymeleaf adds this automatically when th:action is used:
         <input type="hidden" name="_csrf" value="8f1e2a4c-..."> -->

    <input type="text" name="name" />
    <input type="email" name="email" />
    <button type="submit">Create User</button>
</form>

<!-- If NOT using th:action (plain action attribute): -->
<!-- You must include the token manually: -->
<form action="/users" method="post">
    <input type="hidden"
           th:name="${_csrf.parameterName}"
           th:value="${_csrf.token}" />
    <input type="text" name="name" />
    <button type="submit">Create User</button>
</form>

<!-- AJAX requests from Thymeleaf pages — include token in meta tags: -->
<head>
    <meta name="_csrf"        th:content="${_csrf.token}" />
    <meta name="_csrf_header" th:content="${_csrf.headerName}" />
</head>

<script>
    // Read CSRF token from meta tags and include in AJAX requests:
    const token  = document.querySelector('meta[name="_csrf"]').content;
    const header = document.querySelector('meta[name="_csrf_header"]').content;

    fetch('/api/users', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            [header]: token    // X-CSRF-TOKEN: <token>
        },
        body: JSON.stringify({ name: 'Alice' })
    });
</script>

CSRF for SPAs — Cookie-Based Token

Single-page applications (React, Angular, Vue) that use session cookies for authentication need CSRF protection. The CookieCsrfTokenRepository stores the token in a readable cookie (XSRF-TOKEN) that the JavaScript framework reads and sends back as a header (X-XSRF-TOKEN). Angular's HttpClient does this automatically.
Java
// ── CookieCsrfTokenRepository — SPA-friendly CSRF: ───────────────────
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf
            // Store CSRF token in a cookie readable by JavaScript:
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            // withHttpOnlyFalse() — JavaScript can read the XSRF-TOKEN cookie.
            // The default is HttpOnly=true (server-side only).
        )
        .csrf(csrf -> csrf
            .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
            // Required in Spring Security 6 to handle the BREACH mitigation
            // correctly with cookie-based tokens.
        );
    return http.build();
}

# ── What this sets up: ────────────────────────────────────────────────
# GET /any-endpoint → Response sets cookie: XSRF-TOKEN=8f1e2a4c-...
# SPA reads the cookie and sends the value back in: X-XSRF-TOKEN header

// ── Angular — automatic CSRF: ────────────────────────────────────────
// Angular's HttpClientModule reads XSRF-TOKEN cookie and sends
// X-XSRF-TOKEN header automatically. No configuration needed.
// Just import HttpClientModule and HttpClientXsrfModule:
// imports: [HttpClientModule, HttpClientXsrfModule]

// ── React/Vue — manual CSRF: ──────────────────────────────────────────
function getCookie(name) {
    const match = document.cookie.match(
        new RegExp('(^| )' + name + '=([^;]+)'));
    return match ? match[2] : null;
}

// Include in every state-changing request:
fetch('/api/users', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-XSRF-TOKEN': getCookie('XSRF-TOKEN')
    },
    credentials: 'include',   // include session cookie
    body: JSON.stringify({ name: 'Alice' })
});

Disabling CSRF for REST APIs

Stateless REST APIs that authenticate with JWT bearer tokens do not need CSRF protection. Browsers do not auto-attach Authorization headers, so CSRF attacks cannot work. Disabling CSRF removes the filter from the chain entirely.
Java
// ── Disable CSRF for stateless REST APIs: ────────────────────────────
@Bean
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf.disable())      // CSRF not needed for stateless JWT auth
        .sessionManagement(session -> session
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        );
    return http.build();
}

// ── Disable CSRF for specific paths only (API + web app in one app): ──
@Bean
public SecurityFilterChain mixedFilterChain(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf
            .ignoringRequestMatchers("/api/**")   // no CSRF for REST endpoints
            // Web endpoints at /** still protected by CSRF
        );
    return http.build();
}

// ── Partial disable by request matcher: ──────────────────────────────
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf
            .requireCsrfProtectionMatcher(request -> {
                // Only protect form-based endpoints:
                String path = request.getRequestURI();
                return !path.startsWith("/api/")
                    && !path.startsWith("/actuator/");
            })
        );
    return http.build();
}