CORS Explained: Cross-Origin Resource Sharing Guide
Cross-Origin Resource Sharing (CORS) is a security mechanism built into web browsers that controls how web pages from one origin (domain, protocol, port) can request resources from a different origin. Without CORS, browsers enforce the Same-Origin Policy (SOP), which blocks cross-origin HTTP requests made by JavaScript. CORS provides a safe, standardized way to relax this restriction when needed.
The Same-Origin Policy
The Same-Origin Policy is one of the fundamental security mechanisms of the web. Two URLs have the same origin if they share the same protocol, domain, and port:
| URL A | URL B | Same Origin? | Reason |
|---|---|---|---|
| SSL/TLS проверку://example.com/page1 | https://example.com/page2 | Yes | Same protocol, domain, port |
| https://example.com | http://example.com | No | Different protocol |
| https://example.com | https://API документацию.example.com | No | Different subdomain |
| https://example.com | https://example.com:8080 | No | Different port |
| https://example.com | https://other.com | No | Different domain |
Without the Same-Origin Policy, any website could read data from your banking app, email, or social media accounts using JavaScript — as long as you were logged in.
How CORS Works
CORS works through HTTP headers. When a browser makes a cross-origin request, it includes an Origin header. The server responds with Access-Control-* headers indicating whether the request is allowed:
# Browser sends:
GET /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
# Server responds:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json
{"data": "response"}
If the server does not include the appropriate CORS headers, the browser blocks the response and logs a CORS error in the console.
Simple Requests vs Preflight Requests
The browser categorizes cross-origin requests into two types:
Simple Requests
A request is "simple" if it meets all of these conditions:
- Method is GET, HEAD, or POST
- Only "safe" headers are used: Accept, Accept-Language, Content-Language, Content-Type
- Content-Type is one of: application/x-www-form-urlencoded, multipart/form-data, text/plain
Simple requests are sent directly to the server. The browser checks the response headers after the fact.
Preflight Requests
Any request that does not qualify as "simple" triggers a preflight — an automatic OPTIONS request that the browser sends before the actual request:
# Step 1: Browser sends preflight
OPTIONS /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization
# Step 2: Server responds to preflight
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
# Step 3: Browser sends the actual request
PUT /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGci...
{"key": "value"}
CORS Headers Reference
| Header | Direction | Purpose |
|---|---|---|
Origin | Request | Tells the server where the request comes from |
Access-Control-Allow-Origin | Response | Specifies which origins can access the resource |
Access-Control-Allow-Methods | Response | Allowed HTTP methods for preflight |
Access-Control-Allow-Headers | Response | Allowed request headers for preflight |
Access-Control-Allow-Credentials | Response | Whether cookies/auth headers are allowed |
Access-Control-Expose-Headers | Response | Response headers the browser can access via JS |
Access-Control-Max-Age | Response | How long to cache preflight results (seconds) |
Access-Control-Request-Method | Request | Method the actual request will use (preflight) |
Access-Control-Request-Headers | Request | Headers the actual request will use (preflight) |
Common CORS Configurations
Allow a Specific Origin
# Nginx
location /api/ {
add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
add_header Access-Control-Max-Age 86400 always;
if ($request_method = OPTIONS) {
return 204;
}
proxy_pass http://backend;
}
Allow Multiple Origins (Dynamic)
# Nginx — check Origin against allowed list
map $http_origin $cors_origin {
default "";
"https://app.example.com" $http_origin;
"https://admin.example.com" $http_origin;
"https://staging.example.com" $http_origin;
}
server {
location /api/ {
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Vary Origin always;
}
}
PHP CORS Handler
$allowedOrigins = [
'https://app.example.com',
'https://admin.example.com',
];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowedOrigins, true)) {
header("Access-Control-Allow-Origin: $origin");
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Max-Age: 86400');
header('Vary: Origin');
}
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
CORS with Credentials
When requests include cookies or authorization headers, additional rules apply:
Access-Control-Allow-Credentials: truemust be setAccess-Control-Allow-Origincannot be*— it must be the specific origin- The client must set
credentials: 'include'in the fetch options
// Client-side JavaScript
fetch('https://api.example.com/data', {
method: 'GET',
credentials: 'include', // Send cookies cross-origin
headers: {
'Authorization': 'Bearer token123'
}
});
Common CORS Errors and Solutions
| Error | Cause | Solution |
|---|---|---|
| No 'Access-Control-Allow-Origin' header | Server does not send CORS headers | Add CORS headers to server response |
| Origin not allowed | Server allows different origin(s) | Add your origin to the allowed list |
| Preflight response is not successful | OPTIONS request returns error | Handle OPTIONS method on the server, return 204 |
| Credentials flag is true but Allow-Origin is * | Wildcard not allowed with credentials | Set specific origin instead of wildcard |
| Request header not allowed | Custom header not in Allow-Headers | Add the header to Access-Control-Allow-Headers |
Security Best Practices
- Never use
Access-Control-Allow-Origin: *for authenticated endpoints — it allows any website to make authenticated requests - Validate the Origin header server-side — do not blindly reflect it back
- Limit allowed methods — only allow methods the API actually uses
- Limit exposed headers — only expose headers the client actually needs
- Set Access-Control-Max-Age — reduce preflight frequency (86400 = 24 hours)
- Use Vary: Origin — when dynamically selecting allowed origins, ensure caches distinguish responses by origin
CORS and Monitoring
For web monitoring, CORS affects how monitoring tools interact with APIs:
- Browser-based monitoring (Real User Monitoring) is subject to CORS restrictions
- Server-side monitoring is not affected by CORS — it is a browser-only mechanism
- When testing APIs from the browser, CORS errors may mask actual server errors
- Preflight caching (Max-Age) affects how quickly CORS policy changes take effect
Summary
CORS is a browser security mechanism that controls cross-origin HTTP requests. It uses HTTP headers to determine whether a web page from one origin can access resources from another. Simple requests are sent directly; complex requests trigger a preflight OPTIONS check. Proper CORS configuration requires specifying allowed origins, methods, and headers — and being especially careful with credentialed requests. Understanding CORS is essential for anyone building or consuming web APIs, as misconfiguration leads to either security vulnerabilities or broken functionality.
Check your website right now
Check now →