Before diving into CORS, we must understand the Same-Origin Policy (SOP). It's a fundamental security mechanism built into web browsers.
SOP restricts how a document or script loaded from one origin can interact with resources from another origin. This prevents malicious scripts on one website from reading sensitive data or performing unauthorized actions on another website you might be logged into.
An origin is defined by the combination of:
http,
https)
www.example.com,
api.example.com)
80, 443,
8080) - Note: Default ports (80 for http, 443 for
https) are often implied if not specified.
Two URLs have the same origin only if all three parts match.
Examples:
http://example.com/page.html vs
http://example.com/other.html ->
Same Origin
http://example.com vs
https://example.com ->
Different Origin
(Scheme mismatch)
http://www.example.com vs
http://api.example.com ->
Different Origin
(Hostname mismatch)
http://example.com vs
http://example.com:8080 ->
Different Origin
(Port mismatch)
SOP primarily applies to scripts making requests (e.g., using `XMLHttpRequest` or the `fetch` API). Loading resources like images , scripts , and stylesheets is generally allowed across origins, though scripts have limited interaction capabilities with the embedding page due to SOP.
While SOP is crucial for security, it poses a challenge for modern web
applications. Often, a web application hosted on one origin (e.g.,
https://my-app.com) needs to fetch data from an API
hosted on a different origin (e.g.,
https://api.my-app.com or
https://api.third-party.com).
By default, SOP would block these requests made by JavaScript.
Cross-Origin Resource Sharing (CORS) is the mechanism that allows servers to explicitly relax the SOP for specific origins. It's a system, based on HTTP headers, that tells browsers "It's okay for scripts from Origin A to make requests to me (Server B)".
Crucially: CORS is enforced by the browser, but it's configured by the server. The server sends specific HTTP headers in its response to indicate which origins, methods, or headers are permitted.
When JavaScript code attempts a cross-origin request, the browser
automatically adds an Origin request header indicating
the origin of the script making the request.
Origin: https://requesting-site.com
The server then looks at this Origin header and decides,
based on its configuration, whether to allow the request. If allowed,
it includes specific Access-Control-* headers in its
response.
The browser receives the response, examines the CORS headers, and determines if the server permitted the request from the specific origin. If permitted, the browser passes the response data to the JavaScript code. If not, the browser blocks the response from reaching the JavaScript, and you'll typically see a CORS error in the developer console.
There are two main types of CORS requests handled by browsers:
A request qualifies as "simple" if it meets all the following conditions:
GET, HEAD, POST.
Origin, User-Agent) only
include CORS-safelisted headers, such
as:
AcceptAccept-LanguageContent-LanguageContent-Type (with a value of
application/x-www-form-urlencoded,
multipart/form-data, or
text/plain only)
For simple requests, the browser makes the request directly, includes
the Origin header, and checks the response for a suitable
Access-Control-Allow-Origin header.
Browser -> Server: GET /data
Origin: https://requesting-site.com
Server -> Browser: HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://requesting-site.com (or *)
Content-Type: application/json
{ "data": "..." }
If Access-Control-Allow-Origin matches the request's
Origin (or is *), the browser allows the
JavaScript code to access the response.
Requests that do not meet the "simple request" criteria are considered "preflighted". This commonly happens if:
PUT, DELETE, PATCH).
Authorization, X-Requested-With).
Content-Type header is set to something non-simple
(e.g., application/json).
For these requests, the browser first sends a preliminary
"preflight" request using the
OPTIONS method to the
server. This OPTIONS request asks the server for permission for the
*actual* request that the script wants to make.
The preflight request includes:
Origin:
https://requesting-site.com
Access-Control-Request-Method:
PUT
(The method the actual request will use)
Access-Control-Request-Headers:
Content-Type, X-Custom-Header
(Any non-simple headers the actual request will include)
Browser -> Server (Preflight): OPTIONS /resource/123
Origin: https://requesting-site.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, X-Custom-Header
Server -> Browser (Preflight Response): HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://requesting-site.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, X-Custom-Header
Access-Control-Max-Age: 86400 (Optional: Cache preflight result for 1 day)
The server responds to the OPTIONS request with headers indicating what is allowed:
Access-Control-Allow-Origin:
Must match the requesting origin (or be *, though less
common for preflights).
Access-Control-Allow-Methods:
Must include the method specified in
`Access-Control-Request-Method`.
Access-Control-Allow-Headers:
Must include the headers specified in
`Access-Control-Request-Headers`.
If the preflight response indicates permission (all checks pass), the
browser then sends the actual request
(e.g., the PUT request in this example). The server
responds to this actual request, and again, that response
must still include a suitable
Access-Control-Allow-Origin header for the browser to
allow the JavaScript access to the final response.
If the preflight check fails, the actual request is never sent, and the browser reports a CORS error.
Understanding these headers is essential for debugging CORS issues.
Origin: Indicates the origin of the cross-origin request. Always sent for
cross-origin requests.
Access-Control-Request-Method: Sent in preflight (OPTIONS) requests. Indicates the HTTP method
the actual request will use.
Access-Control-Request-Headers: Sent in preflight (OPTIONS) requests. Indicates the non-simple
HTTP headers the actual request will use (comma-separated).
Access-Control-Allow-Origin:
The most important header. Specifies
which origin(s) are allowed to access the resource. Can be a
specific origin (e.g., https://example.com) or
* (allowing any origin). Must be present on responses
to both simple and actual (post-preflight) requests. Also required
on preflight responses.
Access-Control-Allow-Methods: Sent in response to preflight requests. Specifies which HTTP
methods are allowed for the actual request (comma-separated list,
e.g., GET, POST, PUT).
Access-Control-Allow-Headers: Sent in response to preflight requests. Specifies which HTTP
headers are allowed in the actual request (comma-separated list,
e.g., Content-Type, Authorization).
Access-Control-Allow-Credentials: (Value: true) Indicates whether the browser should
expose the response to the JavaScript code when the request includes
credentials (like cookies or HTTP Authentication).
If this is set to true,
Access-Control-Allow-Origin
cannot be *; it must
be a specific origin.
Access-Control-Expose-Headers: By default, browsers only expose certain "safe" response headers
to JavaScript (like `Content-Type`). This header allows the server
to list additional response headers (e.g., a custom header like
`X-RateLimit-Remaining`) that should be made accessible to the
script (comma-separated list).
Access-Control-Max-Age: Sent in response to preflight requests. Specifies how long (in
seconds) the results of the preflight request can be cached by the
browser, avoiding repeated preflight checks for subsequent requests
to the same URL with the same method/headers.
Configure a simulated cross-origin request and the server's CORS policy below. Click "Run Simulation" to see the step-by-step interaction and whether the request would succeed or fail based on CORS rules.
Content-Type to anything other than
text/plain, multipart/form-data, or
application/x-www-form-urlencoded will trigger
preflight. You can include simple headers like `Accept` - they
won't trigger preflight or require `Allow-Headers`.
This simulation demonstrates the CORS negotiation flow based on standard rules. It does not perform real network requests.
When CORS isn't configured correctly, you'll see errors in your browser's developer console. Understanding them is key:
Access to fetch at '...' from origin '...' has been blocked by
CORS policy: No 'Access-Control-Allow-Origin' header is present on
the requested resource.
Access-Control-Allow-Origin header at all. The server
isn't configured for CORS, or it didn't recognize the request
origin.
Access to fetch at '...' from origin '...' has been blocked by
CORS policy: The 'Access-Control-Allow-Origin' header has a value
'...' that is not equal to the supplied origin.
Access-Control-Allow-Origin header, but its value
doesn't match the origin of your web page. Check the server
configuration.
Access to fetch at '...' from origin '...' has been blocked by
CORS policy: Method ... is not allowed by
Access-Control-Allow-Methods in preflight response.
Access-Control-Allow-Methods header didn't include the
HTTP method (e.g., PUT, DELETE) your actual request intended to use.
Access to fetch at '...' from origin '...' has been blocked by
CORS policy: Request header field ... is not allowed by
Access-Control-Allow-Headers in preflight response.
Access-Control-Allow-Headers response to the preflight
request.
Access to fetch at '...' from origin '...' has been blocked by
CORS policy: The value of the 'Access-Control-Allow-Origin' header
in the response must not be the wildcard '*' when the request's
credentials mode is 'include'.
Access-Control-Allow-Origin: *. For
credentialed requests, the server *must* specify the exact allowed
origin.
Access to fetch at '...' from origin '...' has been blocked by
CORS policy: Response to preflight request doesn't pass access
control check: The value of the 'Access-Control-Allow-Credentials'
header in the response is '' which must be 'true' when the
request's credentials mode is 'include'.
Access-Control-Allow-Credentials: true in the preflight
(or actual) response.
Debugging Tips:
Origin request header and all
Access-Control-* response headers. Look at both the
OPTIONS (preflight) and the actual requests.
How you configure CORS depends heavily on your server-side technology (Node.js/Express, Python/Flask/Django, Java/Spring, Apache, Nginx, etc.). The core idea is to intercept requests, check the `Origin` header, and add the appropriate `Access-Control-*` response headers based on your policy.
const express = require('express');
const cors = require('cors'); // npm install cors
const app = express();
// Option 1: Allow all origins (use with caution)
// app.use(cors());
// Option 2: Allow specific origin
const corsOptions = {
origin: 'https://www.client-app.com',
methods: ['GET', 'POST', 'PUT'], // Allowed methods for preflight
allowedHeaders: ['Content-Type', 'Authorization'], // Allowed headers for preflight
credentials: true, // Allow cookies
optionsSuccessStatus: 204 // Return 204 for preflight OPTIONS requests
};
app.use(cors(corsOptions));
// Your API routes follow...
app.get('/api/data', (req, res) => {
res.json({ message: 'Data fetched successfully!' });
});
app.listen(3000, () => console.log('Server listening on port 3000'));
server {
listen 80;
server_name api.server.com;
location / {
# Define allowed origin
set $cors_origin "";
if ($http_origin ~* "^https?://(www\.client-app\.com|another-client\.com)$") {
set $cors_origin $http_origin;
}
# Handle Preflight OPTIONS requests
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' "$cors_origin" always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With' always;
add_header 'Access-Control-Allow-Credentials' 'true' always; # If needed
add_header 'Access-Control-Max-Age' 1728000 always; # Cache for 20 days
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
# Handle Actual requests - add Allow-Origin if it matched
if ($cors_origin != "") {
add_header 'Access-Control-Allow-Origin' "$cors_origin" always;
add_header 'Access-Control-Allow-Credentials' 'true' always; # If needed
# add_header 'Access-Control-Expose-Headers' 'Content-Length, X-My-Custom-Header'; # If needed
}
# Proxy to your actual backend application
proxy_pass http://your_backend_app_address;
# ... other proxy settings
}
}
These are simplified examples. Real-world configurations might need more nuanced logic, especially for handling multiple origins or complex header requirements.
Access-Control-Allow-Origin: * if
possible,
especially if dealing with sensitive data or sessions (cookies). It
allows *any* website to make requests to your API. Be specific about
trusted origins.
Access-Control-Allow-Credentials: true, you
must specify an exact origin in
Access-Control-Allow-Origin, not *.
Access-Control-Allow-Methods and
Access-Control-Allow-Headers. Only allow what's
necessary.