What is CORS?
CORS (Cross-Origin Resource Sharing) is one of the most commonly encountered—and often frustrating—concepts for web developers. It's a browser security mechanism that controls how web pages can request resources from a different domain (origin) than the one that served the page.
If you've ever seen the dreaded error message "has been blocked by CORS policy", you've experienced this security mechanism in action. Understanding CORS is essential because it affects virtually every modern web application that separates frontend and backend.
Why Does CORS Exist? The Security Problem
To understand CORS, you first need to understand the threat it protects against. Imagine this scenario:
- You're logged into your bank at
https://mybank.com - In another tab, you visit a malicious site
https://evil-site.com - Without security restrictions, JavaScript on evil-site.com could:
- Make requests to
https://mybank.com/api/transfer - Read your account information
- Transfer money to the attacker's account
- All using YOUR authenticated session (because cookies are sent automatically)
- Make requests to
This is why browsers implement the Same-Origin Policy—JavaScript can only make requests to the same origin that served the page, unless the target server explicitly allows it via CORS.
The Same-Origin Policy: By default, browsers block JavaScript from making requests to different origins. This is a critical security measure that prevents malicious websites from accessing your data on other sites.
CORS (Cross-Origin Resource Sharing): The mechanism by which servers can explicitly say "I allow requests from these origins." It's not about making your app work—it's about security. CORS is the server giving permission to the browser.
Key Concept: The Browser Enforces CORS, Not the Server
This is a crucial point that confuses many developers: CORS is enforced by the browser, not the server. The server just sends headers saying what it allows—the browser decides whether to block or allow based on those headers.
- If you use
curlor Postman, CORS doesn't apply—these tools don't enforce same-origin policy - CORS only affects requests made by JavaScript in browsers
- The request often actually reaches the server; the browser just blocks JavaScript from reading the response
What is an "Origin"?
// An origin consists of: Protocol + Host + Port
https://example.com:443/path/page
└─┬─┘ └────┬────┘ └┬┘
│ │ └── Port (443 is default for HTTPS)
│ └────────── Host (domain)
└──────────────────── Protocol (scheme)
// Same origin examples:
https://example.com/page1
https://example.com/page2 ✓ Same origin (different path)
// Different origin examples:
https://example.com
http://example.com ✗ Different protocol
https://api.example.com ✗ Different subdomain
https://example.com:8080 ✗ Different port
https://other.com ✗ Different domain
The CORS Problem
// Scenario: Frontend at https://myapp.com, API at https://api.myapp.com
// Frontend JavaScript tries to fetch data:
fetch('https://api.myapp.com/users')
.then(response => response.json())
.then(data => console.log(data));
// Browser blocks it!
// Error: "Access to fetch at 'https://api.myapp.com/users' from origin
// 'https://myapp.com' has been blocked by CORS policy"
// Why? Different subdomains = different origins!
┌─────────────────────────────────────────────────────────────────┐
│ BROWSER │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Page loaded from: https://myapp.com │ │
│ │ │ │
│ │ JavaScript tries: fetch('https://api.myapp.com/users') │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌───────────────────────┐ │ │
│ │ │ CORS CHECK │ │ │
│ │ │ Origin: myapp.com │ │ │
│ │ │ Target: api.myapp │ │ │
│ │ │ ──────────────── │ │ │
│ │ │ DIFFERENT! BLOCK! │ │ │
│ │ └───────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
How CORS Works
Simple Requests
For "simple" requests (GET, POST with simple content types), the browser sends the request but blocks the response if CORS headers are missing.
// 1. Browser sends request with Origin header
GET /api/users HTTP/1.1
Host: api.myapp.com
Origin: https://myapp.com ← Browser adds this automatically
// 2. Server responds with CORS headers
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://myapp.com ← Server allows this origin
Content-Type: application/json
[{"id": 1, "name": "John"}]
// 3. Browser checks: Does Allow-Origin match our origin?
// Yes! Response is allowed through to JavaScript
Preflight Requests
For "non-simple" requests (PUT, DELETE, custom headers, JSON content-type), the browser first sends an OPTIONS request to check if the actual request is allowed.
// Triggering a preflight:
fetch('https://api.myapp.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json', ← Triggers preflight!
'Authorization': 'Bearer token' ← Custom header triggers preflight!
},
body: JSON.stringify({ name: 'John' })
});
// STEP 1: Browser sends preflight OPTIONS request
OPTIONS /api/users HTTP/1.1
Host: api.myapp.com
Origin: https://myapp.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
// STEP 2: Server responds to preflight
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400 ← Cache preflight for 24 hours
// STEP 3: If preflight passes, browser sends actual request
POST /api/users HTTP/1.1
Host: api.myapp.com
Origin: https://myapp.com
Content-Type: application/json
Authorization: Bearer token
{"name": "John"}
// STEP 4: Server responds with data + CORS headers
HTTP/1.1 201 Created
Access-Control-Allow-Origin: https://myapp.com
Content-Type: application/json
{"id": 1, "name": "John"}
- Methods other than GET, HEAD, POST
- Content-Type other than: application/x-www-form-urlencoded, multipart/form-data, text/plain
- Custom headers (Authorization, X-Custom-Header, etc.)
CORS Headers Explained
| Header | Example | Purpose |
|---|---|---|
Access-Control-Allow-Origin |
https://myapp.com or * |
Which origins can access the resource |
Access-Control-Allow-Methods |
GET, POST, PUT, DELETE |
Which HTTP methods are allowed |
Access-Control-Allow-Headers |
Content-Type, Authorization |
Which request headers are allowed |
Access-Control-Allow-Credentials |
true |
Allow cookies/auth with requests |
Access-Control-Expose-Headers |
X-Custom-Header |
Headers JavaScript can read |
Access-Control-Max-Age |
86400 |
How long to cache preflight (seconds) |
Configuring CORS in Spring Boot
Method 1: @CrossOrigin Annotation
// Allow CORS for specific controller
@RestController
@RequestMapping("/api/users")
@CrossOrigin(origins = "https://myapp.com") ← Entire controller
public class UserController {
@GetMapping
public List<User> getUsers() {
return userService.findAll();
}
// Or per method with different settings
@CrossOrigin(
origins = {"https://myapp.com", "https://admin.myapp.com"},
methods = {RequestMethod.GET, RequestMethod.POST},
allowedHeaders = {"Content-Type", "Authorization"},
maxAge = 3600
)
@PostMapping
public User createUser(@RequestBody User user) {
return userService.save(user);
}
}
Method 2: Global CORS Configuration
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**") // Apply to all /api/ endpoints
.allowedOrigins(
"https://myapp.com",
"https://admin.myapp.com"
)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
// Different config for public endpoints
registry.addMapping("/public/**")
.allowedOrigins("*") // Allow all origins
.allowedMethods("GET");
}
}
Method 3: CORS Filter (Most Control)
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
// Allowed origins
config.setAllowedOrigins(Arrays.asList(
"https://myapp.com",
"https://admin.myapp.com"
));
// Or use patterns for dynamic origins
config.setAllowedOriginPatterns(Arrays.asList(
"https://*.myapp.com" // Any subdomain
));
// Allowed methods
config.setAllowedMethods(Arrays.asList(
"GET", "POST", "PUT", "DELETE", "OPTIONS"
));
// Allowed headers
config.setAllowedHeaders(Arrays.asList(
"Content-Type",
"Authorization",
"X-Requested-With"
));
// Exposed headers (readable by JavaScript)
config.setExposedHeaders(Arrays.asList(
"X-Total-Count",
"X-Page-Number"
));
// Allow credentials (cookies, auth headers)
config.setAllowCredentials(true);
// Preflight cache duration
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return new CorsFilter(source);
}
}
Method 4: With Spring Security
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// Enable CORS with default configuration
.cors(Customizer.withDefaults())
// Or with custom configuration
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://myapp.com"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
}
CORS with Credentials
When you need to send cookies or authentication headers with cross-origin requests, special configuration is required.
// Frontend: Must include credentials option
fetch('https://api.myapp.com/users', {
method: 'GET',
credentials: 'include' ← Include cookies!
});
// Or with axios
axios.get('https://api.myapp.com/users', {
withCredentials: true
});
// Backend: Must allow credentials AND cannot use wildcard origin!
// WRONG - Won't work with credentials!
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
// CORRECT - Specific origin required
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Credentials: true
When Access-Control-Allow-Credentials: true, you cannot use * for Allow-Origin. You must specify the exact origin(s).
Dynamic Origin with Credentials
// Allow multiple origins while supporting credentials
@Component
public class DynamicCorsFilter extends OncePerRequestFilter {
private static final Set<String> ALLOWED_ORIGINS = Set.of(
"https://myapp.com",
"https://admin.myapp.com",
"https://mobile.myapp.com"
);
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws Exception {
String origin = request.getHeader("Origin");
if (origin != null && ALLOWED_ORIGINS.contains(origin)) {
// Echo back the specific requesting origin
response.setHeader("Access-Control-Allow-Origin", origin);
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, OPTIONS");
response.setHeader("Access-Control-Allow-Headers",
"Content-Type, Authorization");
response.setHeader("Access-Control-Max-Age", "3600");
}
// Handle preflight
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
return;
}
chain.doFilter(request, response);
}
}
Common CORS Errors and Solutions
Error 1: No 'Access-Control-Allow-Origin' header
// Error message:
// "No 'Access-Control-Allow-Origin' header is present on the requested resource"
// Cause: Server not sending CORS headers
// Solution: Add CORS configuration to your backend
@CrossOrigin(origins = "https://myapp.com")
@GetMapping("/api/data")
public Data getData() { ... }
Error 2: Preflight request doesn't pass
// Error message:
// "Response to preflight request doesn't pass access control check"
// Cause: OPTIONS request failing or not handled
// Solution: Ensure OPTIONS method is allowed
config.setAllowedMethods(Arrays.asList(
"GET", "POST", "PUT", "DELETE",
"OPTIONS" ← Don't forget this!
));
Error 3: Credentials not supported with wildcard
// Error message:
// "The value of 'Access-Control-Allow-Origin' header must not be '*'
// when credentials mode is 'include'"
// Cause: Using * with credentials: 'include'
// Solution: Specify exact origin
// WRONG
config.setAllowedOrigins(List.of("*"));
config.setAllowCredentials(true);
// CORRECT
config.setAllowedOrigins(List.of("https://myapp.com"));
config.setAllowCredentials(true);
Error 4: Header not allowed
// Error message:
// "Request header field X-Custom-Header is not allowed"
// Cause: Custom header not in Allow-Headers
// Solution: Add the header to allowed headers
config.setAllowedHeaders(Arrays.asList(
"Content-Type",
"Authorization",
"X-Custom-Header" ← Add your custom header
));
// Or allow all headers
config.setAllowedHeaders(List.of("*"));
Environment-Specific CORS
// Different CORS settings per environment
@Configuration
public class CorsConfig {
@Value("${cors.allowed-origins}")
private String[] allowedOrigins;
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Arrays.asList(allowedOrigins));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
// application-dev.properties
cors.allowed-origins=http://localhost:3000,http://localhost:5173
// application-prod.properties
cors.allowed-origins=https://myapp.com,https://admin.myapp.com
CORS vs CSRF
| Aspect | CORS | CSRF |
|---|---|---|
| Purpose | Control cross-origin requests | Prevent forged requests |
| Enforced By | Browser | Server |
| Protection | Prevents reading cross-origin responses | Prevents unauthorized actions |
| Mechanism | HTTP headers | Tokens, SameSite cookies |
CORS does NOT prevent requests from being sent - it only prevents the browser from reading the response. A malicious site can still send requests (which might trigger side effects). Use CSRF protection for state-changing operations!
Testing CORS
// Using curl to test CORS headers
# Simple request
curl -v -H "Origin: https://myapp.com" \
https://api.myapp.com/api/users
# Preflight request
curl -v -X OPTIONS \
-H "Origin: https://myapp.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type, Authorization" \
https://api.myapp.com/api/users
# Check response headers:
# < Access-Control-Allow-Origin: https://myapp.com
# < Access-Control-Allow-Methods: GET, POST, PUT, DELETE
# < Access-Control-Allow-Headers: Content-Type, Authorization
# < Access-Control-Allow-Credentials: true
Browser DevTools
// In browser console, check for CORS errors:
// 1. Open DevTools (F12)
// 2. Go to Network tab
// 3. Make your request
// 4. Click on the request
// 5. Check "Headers" tab for:
// - Request Headers: Origin
// - Response Headers: Access-Control-*
// 6. Check Console tab for CORS error messages
Best Practices
DO:
- Be specific with origins - List exact allowed origins
- Use HTTPS - Both for origin and API
- Set Max-Age - Cache preflight responses
- Limit allowed methods - Only what you need
- Configure differently per environment - Stricter in production
- Use allowedOriginPatterns for subdomains -
https://*.myapp.com
DON'T:
- Don't use * in production - Too permissive
- Don't use * with credentials - Doesn't work anyway
- Don't forget OPTIONS method - Needed for preflight
- Don't rely on CORS alone for security - Add authentication!
- Don't expose sensitive headers unnecessarily
Summary
- CORS: Browser security mechanism for cross-origin requests
- Origin: Protocol + Host + Port must all match
- Preflight: OPTIONS request for "non-simple" requests
- Allow-Origin: Which origins can access the resource
- Credentials: Cannot use
*with credentials - Configuration: @CrossOrigin, WebMvcConfigurer, or Filter
- Security: CORS prevents reading responses, not sending requests