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.