How does CORS work?
CORS, or Cross-Origin Resource Sharing, is a browser security mechanism that enables controlled access to resources located outside of a given domain. It allows web applications from one domain to request resources from another domain securely. Without CORS, the browser's Same-Origin Policy (SOP) would strictly prevent such cross-origin requests, protecting users from malicious scripts.
The Same-Origin Policy (SOP)
The Same-Origin Policy is a critical security feature implemented in all modern web browsers. It restricts how a document or script loaded from one origin can interact with a resource from another origin. An origin is defined by the protocol, host (domain), and port. For example, a script at 'https://example.com:8080' cannot directly access data from 'http://api.example.com' or 'https://example.com'.
The SOP prevents malicious websites from reading sensitive data from other sites (e.g., banking sites) that a user might be logged into. However, in modern web development, it's common and legitimate for web applications to need to access resources from different origins (e.g., an API hosted on a separate domain).
What is CORS?
CORS is a W3C standard that provides a way for browsers and servers to communicate whether it's safe to allow cross-origin requests. It extends the SOP, allowing servers to explicitly whitelist specific origins or patterns of origins that are permitted to make requests to their resources. It relies on specific HTTP headers to facilitate this communication.
The browser enforces the CORS policy. When a cross-origin request is made, the browser checks the server's response headers. If the headers indicate that the origin of the requesting page is permitted, the browser allows the JavaScript code to access the response. Otherwise, the browser blocks the response, preventing the JavaScript from seeing the data.
How CORS Works: Simple Requests
Certain types of cross-origin requests are considered 'simple requests'. These are requests that meet a specific set of criteria, typically:
- Method is GET, POST, or HEAD.
- Headers are limited to
Accept,Accept-Language,Content-Language,Content-Type(with specific values likeapplication/x-www-form-urlencoded,multipart/form-data, ortext/plain).
For simple requests, the browser sends the request directly, adding an Origin header that indicates the domain of the requesting script.
GET /data HTTP/1.1
Host: api.example.com
Origin: https://www.frontend.com
User-Agent: Mozilla/5.0 (...)
The server then processes the request. If it's configured to allow requests from https://www.frontend.com, it includes an Access-Control-Allow-Origin header in its response.
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.frontend.com
Content-Type: application/json
Content-Length: 123
{"message": "Data from API"}
The browser receives the response. It checks if the value of Access-Control-Allow-Origin matches the Origin of the request or is a wildcard (*). If it matches, the browser allows the frontend script to access the response. If not, the browser blocks the response, and an error is typically shown in the browser's developer console.
How CORS Works: Preflight Requests
Requests that are not 'simple' are considered 'preflighted requests'. These include requests using HTTP methods other than GET, POST, or HEAD (e.g., PUT, DELETE, PATCH), or requests that include custom headers, or requests with a Content-Type other than application/x-www-form-urlencoded, multipart/form-data, or text/plain (e.g., application/json).
Before sending the actual request, the browser first sends an OPTIONS request to the server. This OPTIONS request is called a 'preflight request'. It asks the server for permission to send the actual request.
The preflight request includes headers like Origin, Access-Control-Request-Method, and Access-Control-Request-Headers to inform the server about the actual request's method and headers.
OPTIONS /data HTTP/1.1
Host: api.example.com
Origin: https://www.frontend.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization
User-Agent: Mozilla/5.0 (...)
The server, upon receiving the preflight request, responds with headers indicating what methods, headers, and origins it permits. If the server is configured to allow the intended request, it responds with headers like:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://www.frontend.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
If the preflight response indicates that the actual request is allowed, the browser then proceeds to send the actual request (e.g., a PUT request). If the preflight response denies permission (e.g., by not including the requested method or headers in Access-Control-Allow-Methods or Access-Control-Allow-Headers), the browser blocks the actual request and shows a CORS error.
Key CORS Headers
- Origin (Request Header): Sent by the browser, indicates the origin (protocol, domain, port) of the resource making the cross-origin request.
- Access-Control-Allow-Origin (Response Header): Sent by the server, specifies which origins are allowed to access the resource. Can be a specific origin or
*(wildcard, allowing any origin). - Access-Control-Request-Method (Request Header, preflight only): Sent by the browser in a preflight request, indicates the HTTP method that will be used in the actual request.
- Access-Control-Request-Headers (Request Header, preflight only): Sent by the browser in a preflight request, indicates the HTTP headers that will be sent in the actual request.
- Access-Control-Allow-Methods (Response Header, preflight only): Sent by the server in a preflight response, lists the HTTP methods allowed for the resource.
- Access-Control-Allow-Headers (Response Header, preflight only): Sent by the server in a preflight response, lists the HTTP headers allowed for the actual request.
- Access-Control-Max-Age (Response Header, preflight only): Sent by the server in a preflight response, indicates how long the results of a preflight request can be cached (in seconds), avoiding repeated preflights.
- Access-Control-Allow-Credentials (Response Header): Sent by the server, indicates whether the response to the request can be exposed when the
credentialsflag is true. This is used when the frontend needs to send cookies, HTTP authentication credentials, or client-side SSL certificates. If this header is present andtrue,Access-Control-Allow-Origincannot be*.
Common CORS Scenarios and Troubleshooting
Using Access-Control-Allow-Origin: * allows any domain to access your resources, which can be convenient for public APIs but less secure for sensitive data. For specific frontend applications, it's better to specify the exact origin (e.g., Access-Control-Allow-Origin: https://my-frontend.com).
When Access-Control-Allow-Credentials: true is set, the Access-Control-Allow-Origin header cannot be *. It must specify a particular origin. This prevents a malicious third-party site from reading sensitive credentialed data from your server.
If you encounter CORS errors, always check the browser's developer console (Network tab and Console tab). It provides detailed information about the blocked request and often the reason for the CORS failure. On the server side, ensure your CORS middleware or configuration is correctly setting the necessary Access-Control-* headers based on the incoming Origin header.
Implementing CORS on the Server-Side (Node.js Example)
Most web frameworks provide middleware or configuration options to handle CORS easily. Here's a common example using Express.js in Node.js with the cors package:
const express = require('express');
const cors = require('cors');
const app = express();
// Basic CORS setup: Allows all origins, methods, and headers
// app.use(cors());
// More restrictive CORS setup
const corsOptions = {
origin: 'https://my-frontend.com', // Only allow this specific origin
methods: ['GET', 'POST', 'PUT', 'DELETE'], // Only allow these methods
allowedHeaders: ['Content-Type', 'Authorization'], // Only allow these headers
credentials: true, // Allow cookies and auth headers
optionsSuccessStatus: 204 // For preflight requests
};
app.use(cors(corsOptions));
app.get('/api/data', (req, res) => {
res.json({ message: 'Hello from API!' });
});
app.listen(3000, () => {
console.log('Server listening on port 3000');
});
In this example, the cors middleware intercepts incoming requests and adds the appropriate Access-Control-* headers to the response based on the configured corsOptions.