Skip to content

CSRF is on by default — don't switch it off

Severity: highApplies to: Spring Security 6.xApplies to: Spring Security 7.xFacts last verified 2026-07-26 against Spring Security 7.1.0

From Spring Security’s own documentation:

Spring Security protects against CSRF attacks by default for unsafe HTTP methods, such as a POST request, so no additional code is necessary.

This is the opposite of Express, where there is no built-in protection and you must choose a library. Here the work is not turning it off, and the reason that needs a page is that turning it off is a one-liner that appears in a great many tutorials and Stack Overflow answers:

// The line to grep for.
http.csrf(csrf -> csrf.disable());

Almost always to make something work, not because anyone decided the risk was acceptable:

  • a JavaScript front end started getting 403s on POST;
  • Postman worked but the browser did not;
  • a REST API “does not need CSRF”.

The third is the only one with a real argument behind it, and it is narrower than it sounds. CSRF exists because browsers attach credentials automatically. An API authenticated purely by an Authorization: Bearer header that the browser does not attach on its own is genuinely not CSRF-exploitable. An API authenticated by a session cookie is — regardless of whether it returns JSON.

So the question is not “is this a REST API” but “does the browser send credentials without my code asking”. If you use cookies, keep CSRF on.

the fix
// Single-page app: hand the token to JavaScript via a cookie.
http.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);

CookieCsrfTokenRepository writes a cookie named XSRF-TOKEN and reads the value back from the X-XSRF-TOKEN header or the _csrf parameter. That is the convention Angular and most HTTP clients already implement, which usually makes the 403s disappear without disabling anything.

The default repository stores the token in the HttpSession instead, which is correct for server-rendered forms and is why Thymeleaf forms need no extra work.

If you genuinely have a stateless token API, scope the exemption rather than removing the protection:

http.csrf(csrf -> csrf.ignoringRequestMatchers("/api/**"));
verify it workedRun this in: http response
Terminal window
curl -si -X POST https://example.com/account/email \
-H 'Cookie: JSESSIONID=<valid-session>' \
-d 'email=attacker@example.test' | head -1

A 403 is the correct result: a session-authenticated POST with no CSRF token must be rejected. A 200 means the protection is off.

Spring Security 6 changed two things that are easy to misread as bugs:

  • Token loading is deferred by default, so the session is not loaded on every request. A token you expected to exist may not have been generated yet.
  • The token includes randomness on every request, as BREACH protection, via XorCsrfTokenRequestAttributeHandler. So the value differs between requests by design — code that caches or compares raw token values will misbehave.

Both are documented defaults, not faults, and both have produced “CSRF is broken” conclusions that ended in csrf.disable().

before you ship this

Keeping CSRF on breaks any client that posts without a token — which is the reason it gets disabled in the first place. Fix the client rather than the server: read the XSRF-TOKEN cookie and send it back as X-XSRF-TOKEN.

Enabling it on an application that has run with it disabled will break integrations you did not know existed. Scope with ignoringRequestMatchers, verify the exempt routes are genuinely not cookie-authenticated, and shrink the exemption over time.