Databases, Spring and microservices · 4. Spring Security, lesson 8 of 8

CSRF, CORS and security headers

Intermediate3 min read@since 17Code runs on your Java 25
Explain it forThe essentials plus production detail and pitfalls.

CSRF (cross-site request forgery): browsers attach cookies to every request for a site, even when another site triggers it. If you authenticate with cookies, a malicious page can make your browser send authenticated requests.

  • Spring Security enables CSRF protection by default: state-changing requests (POST, PUT, DELETE) must include a secret CSRF token that only your own pages know.
  • Server-rendered forms (Thymeleaf) include it automatically. Single-page apps read it from an XSRF-TOKEN cookie and send it back as an X-XSRF-TOKEN header; Angular's HttpClient does this out of the box.
  • SameSite cookies (Lax or Strict) add a second layer.
  • APIs that authenticate with an Authorization header (no cookies) aren't vulnerable, which is the only good reason to disable CSRF.

CORS (cross-origin resource sharing): a browser rule that stops a page from reading responses from another origin unless that origin allows it. It protects users; it is not an access control for your API (curl ignores it). Configure exact allowed origins; * can't be combined with credentials.

Security headers: Spring adds sensible ones by default (X-Content-Type-Options, X-Frame-Options, cache control for authenticated pages, and HSTS over HTTPS). Add a Content-Security-Policy to limit where scripts can load from.

Diagram

How a CORS preflight works

For cross-origin requests that aren't "simple" (JSON bodies, an Authorization header, PUT or DELETE), the browser first sends an OPTIONS preflight asking what is allowed, and only then the real request. The response must carry Access-Control-Allow-Origin for the browser to hand it to the page. Step through it below.

Diagram

The security headers Spring adds

  • X-Content-Type-Options: nosniff: browsers must not guess content types.
  • X-Frame-Options: DENY: your pages can't be framed (clickjacking).
  • Cache-Control: no-cache, no-store: authenticated responses aren't cached.
  • Strict-Transport-Security (HTTPS only): browsers use HTTPS for your site from now on.
  • Not added by default, and worth adding: Content-Security-Policy, which limits where scripts, styles and frames may come from and is your strongest defence against XSS.

Example

Java
@Bean
SecurityFilterChain security(HttpSecurity http) throws Exception {
    return http
        .cors(Customizer.withDefaults())                             // uses the CorsConfigurationSource bean
        .csrf(csrf -> csrf
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())   // XSRF-TOKEN cookie for the SPA
            .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()))
        .headers(headers -> headers
            .contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'; frame-ancestors 'none'"))
            .frameOptions(frame -> frame.deny()))
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .formLogin(Customizer.withDefaults())
        .build();
}

@Bean
CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration cfg = new CorsConfiguration();
    cfg.setAllowedOrigins(List.of("https://javaatlas.com"));         // exact origins, never "*" with credentials
    cfg.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
    cfg.setAllowedHeaders(List.of("Content-Type", "Authorization", "X-XSRF-TOKEN"));
    cfg.setAllowCredentials(true);
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/api/**", cfg);
    return source;
}

Common mistake

Calling csrf().disable() on an app that logs users in with session cookies, or "fixing" a CORS error with allowedOrigins("*").

Under the hood

If your frontend and API live on different sites (for example *.vercel.app and *.up.railway.app), cookies marked SameSite=Strict or Lax won't be sent at all, and third-party cookie blocking makes SameSite=None unreliable. Putting both under one site (javaatlas.com and api.javaatlas.com) fixes it, which is exactly why this site's deployment uses its own domains.

Check yourself

Which kind of authentication is vulnerable to CSRF?

How this connects

Where this leads

You've reached the end of this thread. Try a learning path for what's next.

Part of Spring Core and Spring Security in depth.

Was this lesson helpful?

Finished reading? Mark it complete to track your progress.