CSRF Protection: How It Works and When to Disable It
What Is a CSRF Attack?
Cross-Site Request Forgery (CSRF) tricks an authenticated user’s browser into making an unintended request to your application.
The attack:
- Alice is logged into
bank.com— her browser holds a valid session cookie - Alice visits
evil.com evil.comcontains<img src="https://bank.com/transfer?to=attacker&amount=5000">- Alice’s browser fires the request, automatically attaching her
bank.comsession cookie bank.comreceives an authenticated request that Alice never intended to make
The attack works because browsers automatically send cookies with cross-origin requests. CSRF protection adds a secret token that evil.com cannot read (due to the Same-Origin Policy) and that must be present for state-changing requests.
The Synchronizer Token Pattern
Spring Security’s default CSRF defense:
- When a session is created, generate a random CSRF token and store it in the session
- Include the token in every response (as a hidden form field or cookie)
- For every state-changing request (POST, PUT, PATCH, DELETE), require the token in the request
- Compare the submitted token against the session-stored token — reject if they do not match
evil.com can trigger a cross-origin request, but it cannot read the CSRF token from your page (SOP blocks that), so it cannot include the correct token, and the request is rejected.
sequenceDiagram
Browser->>Server: GET /transfer-form
Server->>Browser: HTML with
Browser->>Server: POST /transfer (body: to=alice&amount=100&_csrf=abc123)
Server->>Server: Compare abc123 with session token ✓
Server->>Browser: 200 OK
Note over Browser,Server: Attack scenario
EvilSite->>Server: POST /transfer (body: to=attacker&amount=5000)
Server->>Server: No _csrf token → reject
Server->>EvilSite: 403 Forbidden
CSRF Is On by Default
Spring Security enables CSRF protection by default for all state-changing HTTP methods. No configuration needed for standard form-based applications.
// CSRF is active without any explicit configuration:
http
.formLogin(Customizer.withDefaults())
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
Form-Based Applications
Thymeleaf
Thymeleaf automatically includes the CSRF token in forms:
<form th:action="@{/transfer}" method="post">
<!-- Thymeleaf inserts: <input type="hidden" name="_csrf" value="..."> -->
<input type="text" name="to">
<input type="number" name="amount">
<button type="submit">Transfer</button>
</form>
Plain HTML / JSP
Include the token manually:
<form action="/transfer" method="post">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}">
<input type="text" name="to">
<button type="submit">Transfer</button>
</form>
Accessing the Token in a Controller
@GetMapping("/transfer-form")
public String transferForm(HttpServletRequest request, Model model) {
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
model.addAttribute("_csrf", csrfToken);
return "transfer-form";
}
Single-Page Applications (SPAs)
SPAs make AJAX requests — they cannot use hidden form fields. Use the cookie-to-header pattern.
CookieCsrfTokenRepository
Configure Spring Security to write the CSRF token to a cookie:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);
return http.build();
}
withHttpOnlyFalse() makes the cookie readable by JavaScript (necessary so the SPA can read and include it in request headers).
Reading and Sending from the SPA
// Read the token from the cookie
function getCsrfToken() {
return document.cookie
.split(';')
.map(c => c.trim())
.find(c => c.startsWith('XSRF-TOKEN='))
?.split('=')[1];
}
// Include it in every state-changing request
async function transfer(to, amount) {
const response = await fetch('/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': getCsrfToken()
},
body: JSON.stringify({ to, amount })
});
return response.json();
}
Spring Security’s CsrfFilter checks the X-XSRF-TOKEN header. evil.com cannot read the cookie (SOP applies to JavaScript reading cookies from other origins), so the attack fails.
Axios Interceptor
If using Axios, configure the interceptor once:
axios.defaults.xsrfCookieName = 'XSRF-TOKEN';
axios.defaults.xsrfHeaderName = 'X-XSRF-TOKEN';
// Axios reads and sends the token automatically for all requests
Custom CSRF Token Repository
Implement CsrfTokenRepository for custom storage (e.g., database-backed tokens):
public class DatabaseCsrfTokenRepository implements CsrfTokenRepository {
private final CsrfTokenStore store; // your persistence layer
@Override
public CsrfToken generateToken(HttpServletRequest request) {
return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", generateRandomToken());
}
@Override
public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
String sessionId = request.getSession().getId();
if (token == null) {
store.delete(sessionId);
} else {
store.save(sessionId, token.getToken());
}
}
@Override
public CsrfToken loadToken(HttpServletRequest request) {
String sessionId = request.getSession(false) != null
? request.getSession().getId()
: null;
if (sessionId == null) return null;
String token = store.find(sessionId);
return token != null ? new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", token) : null;
}
private String generateRandomToken() {
byte[] bytes = new byte[32];
new SecureRandom().nextBytes(bytes);
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
}
}
When to Disable CSRF
CSRF only matters when the browser automatically attaches credentials (session cookies). Stateless APIs authenticating with JWT Bearer tokens are not vulnerable to CSRF — there is no cookie for the browser to attach.
Safe to disable:
- REST APIs using JWT or API keys in the
Authorizationheader (not cookies) - Webhooks receiving callbacks from external services
- Server-to-server communication (no browser involved)
Not safe to disable:
- Form-based web applications using session cookies
- APIs called from browsers that use cookie-based sessions
- Applications using remember-me cookies
// Correct: disable CSRF for a stateless JWT API
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.csrf(csrf -> csrf.disable())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
// WRONG: disabling CSRF for a form-login application
http.csrf(csrf -> csrf.disable()); // opens the application to CSRF attacks
Excluding Specific Endpoints from CSRF
Sometimes you need to exclude specific paths — for example, a webhook endpoint that must accept unauthenticated POST requests from an external service:
http.csrf(csrf -> csrf
.ignoringRequestMatchers("/webhooks/**", "/api/v1/public/**")
);
Only exclude paths that do not perform state-changing operations on behalf of authenticated users. Webhooks typically verify a signature in the payload or a header instead of relying on CSRF tokens.
CSRF and Spring Security 6 Changes
Spring Security 6 made one important change: CsrfFilter now delays loading the CSRF token until it is actually needed (lazy token loading). This avoids creating sessions on every request just to store a CSRF token.
If you use CookieCsrfTokenRepository, you need to explicitly subscribe to the token to ensure it is written to the response:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new XorCsrfTokenRequestAttributeHandler())
);
return http.build();
}
XorCsrfTokenRequestAttributeHandler was introduced in Spring Security 6 to protect against BREACH attacks by XOR-masking the token before writing it to the response.
Key Takeaways
- CSRF exploits session cookies — it is not a concern for stateless JWT APIs
- Spring Security’s Synchronizer Token Pattern generates a random token per session, requires it on every state-changing request
- Thymeleaf automatically includes CSRF tokens; plain HTML requires explicit
<input type="hidden"> - SPAs use
CookieCsrfTokenRepositorywithwithHttpOnlyFalse()and send the token inX-XSRF-TOKEN - Disable CSRF only for stateless APIs using
Authorizationheaders — never for cookie-based applications - In Spring Security 6, use
XorCsrfTokenRequestAttributeHandlerto protect against BREACH attacks
Next: CORS: Cross-Origin Requests and Preflight Configuration — configure CORS correctly so browsers allow your SPA to call your API without disabling security.