CORS, CSRF, and Security Headers in Spring Boot: A Practical Guide
Master CORS, CSRF, and security headers in Spring Boot 3.x.
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
- ✓Java 17+ and Maven/Gradle installed
- ✓Spring Boot 3.2+ project with spring-boot-starter-web and spring-boot-starter-security
- ✓Basic understanding of HTTP headers and browser security model
- ✓Postman or curl for testing endpoints
- ✓Familiarity with Spring Security configuration (WebSecurityConfigurerAdapter is deprecated)
• CORS (Cross-Origin Resource Sharing) controls which domains can access your API via browser-enforced headers like Access-Control-Allow-Origin. • CSRF (Cross-Site Request Forgery) protection prevents malicious sites from executing authenticated actions; in Spring Boot 3.x, it's enabled by default for stateful apps but often disabled for stateless JWT-based APIs. • Security headers (HSTS, X-Content-Type-Options, X-Frame-Options, CSP) add defense-in-depth against clickjacking, MIME sniffing, and data injection. • Spring Security 6.x uses a lambda-based DSL and requires explicit CORS configuration via CorsConfigurationSource bean. • Always test with actual browser requests and tools like curl -v to verify headers; configuration issues often surface only in production under real cross-origin traffic.
Think of your Spring Boot API as a secured office building. CORS is the receptionist checking IDs at the front door, deciding which visitors (domains) are allowed in. CSRF is a security guard making sure no one sneaks in through a window by forging a badge from a trusted visitor. Security headers are the building's firewalls, locked doors, and surveillance cameras—they don't stop the visitor but make the building much harder to exploit. If you forget any of these, your building might look open for business but is actually wide open to attacks.
You've built a beautiful Spring Boot REST API. It's tested locally, works perfectly with Postman, and you're ready to deploy. Then the frontend team calls: "We're getting CORS errors in the browser." You add @CrossOrigin on every controller, push to production, and next week you find a CSRF vulnerability report from a security audit. Sound familiar? I've been there—twice, on the same project, before I learned to treat CORS, CSRF, and security headers as a single coherent defense layer, not three separate checkboxes.
In this guide, we'll walk through Spring Boot 3.2 with Spring Security 6.2, covering real-world configurations that have survived PCI-DSS audits, handled millions of requests per day, and—most importantly—prevented actual incidents. We'll start with what these protections are, move into the official documentation's blind spots, and end with a production incident that cost a fintech startup $12,000 in fraudulent transactions.
You'll learn why the default @CrossOrigin annotation is rarely sufficient, when to disable CSRF (and when absolutely not to), and how to set security headers that satisfy both security scanners and browser compatibility. By the end, you'll have a configuration template you can drop into any project with confidence. No more guessing, no more firefighting cross-origin issues at 2 AM.
Understanding CORS in Spring Boot 3.x
CORS (Cross-Origin Resource Sharing) is a browser-enforced security mechanism that controls which origins (domains) are allowed to access your API. In Spring Boot 3.x with Spring Security 6.x, CORS configuration has changed significantly from the old WebSecurityConfigurerAdapter approach. The browser sends a preflight OPTIONS request before the actual request, and your server must respond with appropriate Access-Control-* headers.
Here's the most common mistake: using @CrossOrigin on individual controllers or methods. While it works for simple cases, it becomes unmanageable as your API grows. You'll end up with inconsistent origins, missing headers, and debugging nightmares. The correct approach is a global CorsConfigurationSource bean.
Let's look at a production-ready CORS configuration for a SaaS billing API that serves a React frontend on app.example.com and a public REST API for third-party integrations.
What the Official Docs Won't Tell You
The Spring Security reference documentation is technically correct but practically incomplete. Here are three things I learned the hard way:
- CORS and CSRF interact in subtle ways. If you disable CSRF (common for REST APIs), the CORS preflight handler still runs, but the actual request may fail if your security filter chain order is wrong. Spring Security 6.x requires that CorsFilter runs before the security filters. If you're using an older configuration or a custom filter, you'll get cryptic 403 errors for valid cross-origin requests.
- @CrossOrigin on @Controller is not enough for error responses. When Spring Boot returns a 4xx or 5xx error, the error dispatcher doesn't include CORS headers. Your frontend will see a generic CORS error in the browser console, even though the real issue is a 500 on the server. You must configure a global CORS filter that handles all responses, including errors.
- Security headers like X-Frame-Options are silently ignored in modern browsers. Chrome and Firefox have deprecated X-Frame-Options in favor of Content-Security-Policy's frame-ancestors directive. If you only set X-Frame-Options, your app is still vulnerable to clickjacking in newer browsers. Always set both for backward compatibility, but rely on CSP.
CSRF Protection: When to Enable and When to Disable
CSRF (Cross-Site Request Forgery) protection prevents malicious websites from tricking a user's browser into making unauthorized requests to your application. Spring Security enables CSRF by default for any state-changing operation (POST, PUT, DELETE, PATCH). However, the decision to enable or disable CSRF depends entirely on your authentication mechanism.
Enable CSRF when: You use cookie-based authentication (session), your app serves a traditional multi-page application (MPA), or you have forms that submit data. CSRF tokens are typically embedded in forms or sent via a custom header (X-CSRF-TOKEN). Spring Security automatically generates and validates these tokens.
Disable CSRF when: You use stateless authentication like JWT with Bearer tokens (sent via Authorization header), or your API is consumed exclusively by non-browser clients (mobile apps, server-to-server). In these cases, the browser's same-origin policy is not the attack vector—the token itself is the credential.
The gray area is SPAs (Single Page Applications) with JWT stored in cookies. Many teams disable CSRF thinking JWT is sufficient, but if the JWT is sent via a cookie (which the browser automatically attaches), you're still vulnerable. The fix is to store JWT in memory or localStorage and send it via Authorization header, or use SameSite=Strict on the cookie.
Security Headers: The Last Line of Defense
Security headers are HTTP response headers that instruct the browser to enforce additional security policies. They don't prevent all attacks, but they raise the bar significantly. In Spring Boot, you configure these via the HttpSecurity.headers() DSL.
Critical headers for any production API: - Strict-Transport-Security (HSTS): Forces HTTPS only. Set max-age=31536000 and includeSubDomains. Never enable preload without testing. - X-Content-Type-Options: nosniff: Prevents MIME type sniffing. Without this, a browser might interpret a user-uploaded file as executable JavaScript. - X-Frame-Options: DENY or SAMEORIGIN: Prevents clickjacking by controlling whether your site can be embedded in an iframe. - Content-Security-Policy (CSP): The most powerful header. Controls which resources (scripts, styles, images, fonts) can be loaded and executed. A strict CSP can mitigate XSS even if an attacker injects malicious code. - Referrer-Policy: strict-origin-when-cross-origin: Controls how much referrer information is sent with requests. - Permissions-Policy: Controls browser features like geolocation, camera, microphone.
Spring Security 6.x provides a fluent API for all these headers. However, be careful with CSP—too strict and you'll break legitimate functionality like inline scripts or CDN-loaded libraries.
Integrating CORS, CSRF, and Headers in a Single Configuration
In a real-world Spring Boot application, you need all three protections working together. The key is the order of configuration in the SecurityFilterChain. Spring Security 6.x processes filters in a specific sequence: CORS filter first, then CSRF, then security headers, then authentication, then authorization.
Here's a complete configuration for a typical SaaS application that has: - A React frontend on app.example.com - A public API for third-party integrations - Cookie-based authentication for admin users - JWT Bearer token for API clients
Notice how we use different CORS configurations for different paths: the public API allows any origin (no credentials), while the admin API restricts to the frontend domain with credentials enabled. CSRF is enabled only for cookie-authenticated paths.
Testing CORS and Security Headers in Development
You cannot fully test CORS with Postman or curl alone—those tools don't enforce the same-origin policy. You need to simulate actual browser behavior. Here's a practical testing strategy:
- Use curl to verify headers: Run curl -v -X OPTIONS -H "Origin: https://evil.com" -H "Access-Control-Request-Method: POST" https://api.example.com/api/endpoint. Check the response headers for Access-Control-Allow-Origin.
- Create a simple HTML page for browser testing: Host a local HTML file that makes fetch requests to your API. Open it in a browser and check the console for CORS errors. This is the closest you can get to production behavior without deploying.
- Use browser developer tools: The Network tab shows all request and response headers. Look for the presence of security headers like Strict-Transport-Security and Content-Security-Policy.
- Automate with Spring Boot test: Use MockMvc with @WebMvcTest to test CORS configuration programmatically. You can set the Origin header and verify the response includes the correct CORS headers.
- Security header scanners: Tools like Mozilla Observatory or securityheaders.com can scan your deployed endpoints and grade your security header configuration.
Common Pitfalls and How to Avoid Them
After years of debugging CORS and CSRF issues, here are the most common mistakes I've seen—and made:
Pitfall 1: Using @CrossOrigin on every controller. This leads to inconsistent configurations. One developer adds a new controller and forgets the annotation, causing mysterious failures. Solution: Use a global CorsConfigurationSource bean.
Pitfall 2: Disabling CSRF without understanding the implications. Many tutorials say "disable CSRF for REST APIs" without explaining why. If you use cookie-based auth or store JWT in cookies, you still need CSRF. Solution: Understand your authentication mechanism and test with a CSRF attack simulation.
Pitfall 3: Setting CSP too broadly. Allowing 'unsafe-inline' or 'unsafe-eval' defeats the purpose of CSP. But being too strict breaks legitimate functionality. Solution: Start with report-only mode, collect violations, then tighten gradually.
Pitfall 4: Forgetting to configure CORS for WebSocket endpoints. WebSocket connections have their own CORS mechanism (Origin header during handshake). Spring's WebSocket configuration has a separate setAllowedOrigins() method.
Pitfall 5: Ignoring error responses. As mentioned earlier, Spring Boot's error handling doesn't include CORS headers by default. Solution: Implement a custom ErrorController or use a filter that adds headers to all responses.
Production Debugging Guide for CORS and Security Headers
When CORS or security header issues hit production, you need a systematic approach. Here's a step-by-step guide:
Step 1: Verify the request reaches your server. Use curl with -v to simulate the exact request the browser sends. If you get a response, the issue is likely on the browser side (CORS policy mismatch). If you get no response, check network connectivity and DNS.
Step 2: Check the preflight response. For non-simple requests (e.g., POST with JSON content-type), the browser sends an OPTIONS request first. Ensure your server responds with 200 OK and the correct Access-Control-* headers. Use curl -X OPTIONS -H "Origin: https://your-frontend.com" -H "Access-Control-Request-Method: POST" https://api.example.com/endpoint.
Step 3: Examine the actual request headers. If the preflight succeeds but the actual request fails, check that the actual response also includes CORS headers. Spring Security's CORS filter should add them to all responses, but custom filters or error handlers might strip them.
Step 4: Check browser console for CSP violations. If you see a CSP error, the browser will specify which directive was violated (e.g., "Refused to load script because it violates the following Content Security Policy directive..."). Use this to adjust your CSP policy.
Step 5: Enable Spring Security debug logging. Add logging.level.org.springframework.security=DEBUG to application.properties. This will show you which filters are invoked and whether CORS or CSRF tokens are being processed correctly.
The $12,000 CORS Mistake
- Never use origins = "*" in production, especially when credentials (cookies, Authorization headers) are involved.
- CORS and CSRF are complementary—stateless JWT doesn't automatically protect you from CSRF if cookies are still used.
- Always test CORS configuration with actual browser preflight requests (OPTIONS) and verify headers with curl -v.
- Security headers like SameSite and CSP are your last line of defense—configure them early.
HttpSecurity.headers() configuration.curl -v -X OPTIONS -H "Origin: https://your-frontend.com" -H "Access-Control-Request-Method: POST" https://api.example.com/endpointcurl -v -X POST -H "Origin: https://your-frontend.com" -H "Content-Type: application/json" https://api.example.com/endpoint| File | Command / Code | Purpose |
|---|---|---|
| CorsConfig.java | @Configuration | Understanding CORS in Spring Boot 3.x |
| SecurityConfig.java | @Configuration | What the Official Docs Won't Tell You |
| CsrfConfig.java | @Configuration | CSRF Protection |
| SecurityHeadersConfig.java | @Bean | Security Headers |
| CompleteSecurityConfig.java | @Configuration | Integrating CORS, CSRF, and Headers in a Single Configuratio |
| CorsTest.java | @WebMvcTest(controllers = TestController.class) | Testing CORS and Security Headers in Development |
| ErrorCorsFilter.java | @Component | Common Pitfalls and How to Avoid Them |
| application.properties | logging.level.org.springframework.security=DEBUG | Production Debugging Guide for CORS and Security Headers |
Key takeaways
Interview Questions on This Topic
Explain how CORS works at the browser level and how Spring Boot handles it.
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
That's Spring Security. Mark it forged?
6 min read · try the examples if you haven't