Security ยท HTTP
Spring Boot 3 ยท Java 21 ยท Verified Fix

CORS Security Deep Dive

Resolving the 'Access-Control-Allow-Origin' preflight failure in Spring Security 6 โ€” and understanding the security implications of every setting.

Framework
Spring Boot 3.x
Runtime
Java 21 LTS
Stability
Enterprise Grade

Technical Briefing

Cross-Origin Resource Sharing (CORS) errors are among the most frustrating to diagnose because they masquerade as server authentication failures. In Spring Boot 3 / Spring Security 6, CORS must be configured at the Security Filter Chain level โ€” not just via @CrossOrigin or WebMvcConfigurer. If Spring Security intercepts the OPTIONS preflight request before your application code runs, the browser reports a CORS violation regardless of your MVC configuration.

โš  Signal Detected

exception_report.log
FATAL
// Browser DevTools Console
Access to fetch at 'https://api.example.com/users' from origin 
'https://app.example.com' has been blocked by CORS policy: 

Response to preflight request doesn't pass access control check: 
No 'Access-Control-Allow-Origin' header is present on the requested resource.

// Network Tab: OPTIONS /users โ†’ HTTP 401 Unauthorized
// (Not a CORS issue โ€” Spring Security blocked the preflight!)

โ—Ž Trace Analysis

Browsers send a preliminary OPTIONS request (preflight) before any non-simple cross-origin request. Spring Security evaluates this OPTIONS request against your security rules first. Unless you explicitly permit unauthenticated OPTIONS requests, Spring Security returns 401 or 403 โ€” which the browser interprets as a CORS failure, even though it's actually an authentication failure. The solution requires wiring CORS configuration directly into the SecurityFilterChain.

โœฆ Remediation Plan

  1. Define a CorsConfigurationSource @Bean and reference it in your SecurityFilterChain via http.cors(cors -> cors.configurationSource(...)).

  2. Never configure CORS in WebMvcConfigurer only โ€” it runs after Spring Security and will not protect preflight requests.

  3. List specific origins explicitly in production; never use the wildcard '*' with allowCredentials(true) โ€” the browser rejects it.

  4. Set allowedHeaders explicitly, including 'Authorization' and 'Content-Type' โ€” the default allowedHeaders is empty.

  5. Use setMaxAge(3600L) to cache preflight responses in the browser and eliminate redundant OPTIONS round-trips.

Production Implementation
SafeJava 21
// โœ… SPRING SECURITY 6 โ€” COMPLETE CORS SETUP
class="hi-ann">@Configuration
class="hi-ann">@EnableWebSecurity
public class SecurityConfig {

    class="hi-ann">@Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // 1. Wire CORS before any auth checks
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .csrf(csrf -> csrf.disable())  // Use stateless JWT or CSRF tokens separately
            .authorizeHttpRequests(auth -> auth
                .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // Explicit preflight permit
                .anyRequest().authenticated()
            );
        return http.build();
    }

    class="hi-ann">@Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        
        // Explicit origins only โ€” never "*" with credentials
        config.setAllowedOrigins(List.of(
            "https://app.example.com",
            "https://admin.example.com"
        ));
        
        config.setAllowedMethods(List.of("GET","POST","PUT","PATCH","DELETE","OPTIONS"));
        config.setAllowedHeaders(List.of("Authorization","Content-Type","X-Requested-With"));
        config.setExposedHeaders(List.of("X-Total-Count","Location"));
        config.setAllowCredentials(true);
        config.setMaxAge(3600L); // Cache preflight 1 hour
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return source;
    }
}

# application.yml โ€” Environment-specific origins
cors:
  allowed-origins:
    - https://app.example.com
  # dev profile overrides:
  # allowed-origins:
  #   - http://localhost:3000

โŸ Engineering Deep-Dive

The Preflight Request Lifecycle

Before sending a cross-origin request with a custom header (like Authorization) or a non-GET/POST method, the browser automatically issues an OPTIONS preflight. Your server must respond within 200โ€“204 with the correct Access-Control-Allow-* headers. If it doesn't โ€” including if it returns 401 โ€” the browser aborts the actual request and reports a CORS error.

This is why Spring Security's authentication filter is the root cause, not a CORS misconfiguration in MVC.

allowCredentials + Wildcard Origin = Fatal

Setting allowCredentials(true) tells the browser to include cookies or Authorization headers. Browsers explicitly reject Access-Control-Allow-Origin: * combined with Access-Control-Allow-Credentials: true as a security measure. Always enumerate allowed origins when using credentials.

CORS vs. CSRF

CORS limits which origins can make cross-origin requests from the browser. CSRF protection prevents a malicious site from tricking a user's browser into making authenticated requests to your server. For stateless JWT APIs, disable CSRF (the token is not auto-sent). For session-based apps, keep CSRF protection enabled alongside CORS.

โ—‡ Elite Standards

  1. Engineering Rule

    Externalize allowed origins to environment configuration โ€” never hardcode https://localhost in your production security config.

  2. Engineering Rule

    Add an integration test using MockMvc that sends an OPTIONS request and asserts the correct Access-Control-Allow-Origin header.

  3. Engineering Rule

    Pair CORS with a strict Content-Security-Policy header to prevent XSS-based CORS bypass attacks.

FAQ

What causes CORS Security Deep Dive in Spring Boot 3?
Browsers send a preliminary OPTIONS request (preflight) before any non-simple cross-origin request. Spring Security evaluates this OPTIONS request against your security rules first. Unless you explicitly permit unauthenticated OPTIONS requests, Spring Security returns 401 or 403 โ€” which the browser interprets as a CORS failure, even though it's actually an authentication failure. The solution requires wiring CORS configuration directly into the SecurityFilterChain.
How do I fix CORS Security Deep Dive?
Define a CorsConfigurationSource @Bean and reference it in your SecurityFilterChain via http.cors(cors -> cors.configurationSource(...)). Never configure CORS in WebMvcConfigurer only โ€” it runs after Spring Security and will not protect preflight requests. List specific origins explicitly in production; never use the wildcard '*' with allowCredentials(true) โ€” the browser rejects it. Set allowedHeaders explicitly, including 'Authorization' and 'Content-Type' โ€” the default allowedHeaders is empty. Use setMaxAge(3600L) to cache preflight responses in the browser and eliminate redundant OPTIONS round-trips.
Best practice #1 for preventing Security ยท HTTP errors?
Externalize allowed origins to environment configuration โ€” never hardcode https://localhost in your production security config.
Best practice #2 for preventing Security ยท HTTP errors?
Add an integration test using MockMvc that sends an OPTIONS request and asserts the correct Access-Control-Allow-Origin header.
Best practice #3 for preventing Security ยท HTTP errors?
Pair CORS with a strict Content-Security-Policy header to prevent XSS-based CORS bypass attacks.

Feedback

Live
ML

M. Leachouri

Founder & Chief Architect

"I built Kodivio because professional tools shouldn't come at the cost of your privacy. Our mission is to provide enterprise-grade utilities that process data exclusively in your browser."

M. Leachouri is an Expert Web Developer, Data Scientist Engineer, and Systems Architect with a deep specialization in DevOps and Cybersecurity. With over a decade of experience building scalable distributed systems and Zero-Trust architectures, he engineered Kodivio to bridge the gap between high-performance computing and absolute user sovereignty.

Verified Expert
Certified Architect
Full Profile & Mission โ†’