CSRF is on by default — don't switch it off
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());Why it gets disabled
Section titled “Why it gets disabled”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.
// 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/**"));curl -si -X POST https://example.com/account/email \ -H 'Cookie: JSESSIONID=<valid-session>' \ -d 'email=attacker@example.test' | head -1A 403 is the correct result: a session-authenticated POST with no CSRF token must be
rejected. A 200 means the protection is off.
Two behaviours that surprise people
Section titled “Two behaviours that surprise people”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().
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.
Related
Section titled “Related”- SecurityFilterChain — the bean this line lives in
- Express: csurf is archived — the same problem where the framework gives you nothing
- Django CSRF trusted origins — also on by default, configured differently