Skip to content
Spring Boot sb security 3 min read

CSRF Protection

Cross-Site Request Forgery (CSRF) is an attack where a malicious site tricks a logged-in user’s browser into sending an unwanted state-changing request to your application. Spring Security enables CSRF protection by default, and understanding when it helps — and when to turn it off — is essential. The short version: CSRF protection matters for cookie/session-based apps and is usually disabled for stateless token APIs.

What CSRF is

The attack relies on the browser automatically attaching cookies (including your session cookie) to any request to your domain, even one initiated by another site.

1. Victim logs in to bank.com  → browser holds JSESSIONID cookie
2. Victim visits evil.com (still logged in to bank.com)
3. evil.com auto-submits a hidden form:
      POST https://bank.com/transfer   amount=1000&to=attacker
4. Browser attaches the bank.com session cookie automatically
5. Without CSRF protection, bank.com processes the transfer as the victim

The server can’t tell the forged request from a legitimate one because the cookie is valid. CSRF protection adds a secret the attacker cannot read or guess.

How Spring’s CsrfFilter works

Spring’s CsrfFilter uses the synchronizer token pattern. The server issues a random CSRF token tied to the session; every state-changing request (POST, PUT, PATCH, DELETE) must echo that token in a header or form field. Safe methods (GET, HEAD, OPTIONS, TRACE) are exempt.

Because the token lives in server-side state (or a non-HttpOnly cookie the JS reads) rather than being auto-sent like a cookie, a cross-origin attacker cannot supply it. The request is rejected with 403:

HTTP/1.1 403 Forbidden
{"error":"Invalid CSRF Token ... was found on the request parameter '_csrf' or header 'X-CSRF-TOKEN'."}

Server-rendered forms

With Thymeleaf or other server-side templates, CSRF works with zero code — the token is injected into the form automatically:

<form method="post" th:action="@{/transfer}">
    <!-- Thymeleaf adds: <input type="hidden" name="_csrf" value="..."/> -->
    <button type="submit">Transfer</button>
</form>

This is why CSRF protection should stay on for traditional session-based web apps — you get it essentially for free.

SPAs: CookieCsrfTokenRepository

For a JavaScript single-page app that still uses session cookies, store the token in a cookie the JS can read and send back in a header. Use CookieCsrfTokenRepository:

import org.springframework.security.web.csrf.*;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()))
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
    return http.build();
}

withHttpOnlyFalse() lets the front end read the XSRF-TOKEN cookie and send it back as the X-XSRF-TOKEN header. Axios does this automatically; with fetch you read the cookie and set the header yourself.

Note: In Spring Security 6 the token handling changed slightly (CsrfTokenRequestAttributeHandler and deferred token loading). For SPAs you typically expose a small GET endpoint that returns the token so the cookie is set before the first mutating call.

Why stateless JWT APIs disable CSRF

If your API is stateless and authenticates with a Authorization: Bearer <jwt> header rather than a cookie, CSRF protection is unnecessary — and gets in the way.

The reasoning: CSRF exploits the browser’s automatic cookie attachment. A JWT in the Authorization header is not sent automatically by the browser to other origins; the attacker’s page cannot read your token (it’s in localStorage/memory) and cannot set the header on a cross-site request. With no ambient credential to forge, there is nothing for CSRF to protect.

@Bean
public SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf.disable())          // safe: no cookie-based auth
        .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
    return http.build();
}
Auth styleCredential carrierCSRF needed?
Session + cookie (server-rendered)JSESSIONID cookieYes — keep enabled
Session + cookie (SPA)JSESSIONID cookieYes — use cookie token repo
Stateless JWT in headerAuthorization headerNo — disable
JWT stored in a cookiecookieYes — re-enable CSRF

Warning: “Disable CSRF” is correct only when you are not relying on a cookie that the browser sends automatically. If you put your JWT in a cookie, you are back in CSRF territory and must protect it again.

Pitfalls

  • Disabling CSRF on a session/cookie app to “fix” a 403 — you’ve opened a real vulnerability; send the token instead.
  • Forgetting the token on POST forms after enabling CSRF — every mutating request needs it.
  • SPA reading an HttpOnly token cookie — it can’t; use withHttpOnlyFalse() for the CSRF cookie.
Last updated June 13, 2026
Was this helpful?