CSRF, CORS & Security Headers
CSRF protection for stateful apps, CORS configuration for cross-origin frontends, and security headers like HSTS and CSP.
CSRF: protecting stateful, cookie-based apps
Cross-Site Request Forgery exploits the fact that a browser automatically attaches cookies (including a session cookie) to any request to a site, even one triggered by a malicious page the user merely has open in another tab:
<!-- on evil-site.com, while the victim is still logged into your-bank.com -->
<form action="https://your-bank.com/api/transfer" method="POST">
<input type="hidden" name="amount" value="10000">
<input type="hidden" name="toAccount" value="attacker-account">
</form>
<script>document.forms[0].submit()</script>
The browser happily attaches the victim's real session cookie to that request — as far as your-bank.com's server can tell, it's a legitimate, authenticated request. CSRF protection defeats this by requiring a second, unpredictable token that a same-origin page can read but a cross-origin attacker's page cannot.
Spring Security enables CSRF protection by default for any request that relies on cookies/sessions, and issues the token via a repository:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
);
return http.build();
}
CookieCsrfTokenRepository stores the token in a readable cookie (XSRF-TOKEN) that JavaScript on the same origin can read and echo back as a request header (X-XSRF-TOKEN) — a pattern most frontend HTTP clients (Axios, Angular's HttpClient) support automatically once that cookie is present.
| Application shape | CSRF protection |
|---|---|
| Server-rendered forms, session/cookie-based auth | Keep enabled — this is exactly the scenario it protects |
Stateless JSON API, Authorization: Bearer <token> only, no cookies |
Safe to disable — there's no ambient credential a forged cross-origin request could exploit |
CORS: controlling cross-origin requests deliberately
Cross-Origin Resource Sharing is the opposite kind of concern: it's what lets a legitimate frontend on a different origin (https://app.example.com calling an API at https://api.example.com) work at all, since browsers block cross-origin requests by default unless the server explicitly opts in.
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://app.example.com"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
config.setAllowCredentials(true); // needed if the frontend sends cookies
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(Customizer.withDefaults()) // picks up the CorsConfigurationSource bean above
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
return http.build();
}
| CSRF | CORS | |
|---|---|---|
| Protects against | A forged request riding on the victim's own browser session | A browser silently allowing any site's JavaScript to call your API and read the response |
| Default browser behavior it fights | Cookies are attached automatically, regardless of origin | Same-origin policy already blocks this — CORS headers are what relax it |
| Who configures it, and why | The server, to require proof a request came from its own pages | The server, to explicitly allow specific other origins |
| Never disable it for | Cookie/session-based browser apps | Never — CORS misconfiguration (allowedOrigins: "*" with credentials) is a real vulnerability, not just an inconvenience |
Security headers: HSTS and CSP
A few HTTP response headers instruct the browser itself to enforce additional protections. Spring Security sets sensible defaults for most of these automatically, but they're worth understanding:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.headers(headers -> headers
.contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'"))
.httpStrictTransportSecurity(hsts -> hsts.includeSubDomains(true).maxAgeInSeconds(31536000))
);
return http.build();
}
| Header | Purpose |
|---|---|
Strict-Transport-Security (HSTS) |
Tells the browser to only ever contact this site over HTTPS, even if a link or bookmark says http:// — defends against downgrade/stripping attacks |
Content-Security-Policy (CSP) |
Restricts which sources scripts, styles, and other resources can be loaded from — a strong mitigation against cross-site scripting (XSS), since even an injected <script> tag can't execute if it violates the policy |
X-Content-Type-Options: nosniff |
Stops the browser from guessing ("sniffing") a response's content type differently than declared, closing off a class of content-type-confusion attacks |
Common mistakes
- Disabling CSRF protection reflexively on every project, including cookie/session-based server-rendered apps, "because the JWT tutorial said to" — that guidance applies specifically to stateless, cookie-free APIs.
- Setting
allowedOrigins("*")together withallowCredentials(true)— browsers reject this combination outright (and for good reason: it would mean any site anywhere could make credentialed requests), so it fails rather than silently working. - Confusing CSRF and CORS as the same concern — CSRF protects your server from forged requests riding on a victim's own session; CORS is what selectively relaxes the browser's default same-origin restriction for your own legitimate frontend.
- Setting an overly permissive CSP (or none at all) and losing one of the more effective defenses against a successful XSS injection actually being able to do anything.