Security
CORS — Cross-Origin Resource Sharing
CORS is a browser-side security mechanism that enforces an origin-based policy for cross-domain requests. It matters because it prevents malicious scripts from unauthorized access to sensitive API data by verifying server-side permissions. You must implement CORS when your web-based frontend needs to communicate with an API hosted on a different domain, subdomain, or port.
The Same-Origin Policy Foundation
To understand CORS, you must first grasp the browser's default behavior: the Same-Origin Policy (SOP). This security mandate restricts how a document or script loaded from one origin can interact with a resource from another. An origin is defined by the combination of protocol, host, and port. Without SOP, a malicious website could open a tab to your banking portal and execute unauthorized requests using your active session cookies. SOP keeps your traffic contained within its trusted domain. However, modern web architectures often require distributed services, where a frontend hosted on 'app.com' must consume data from an API on 'api.service.com'. Because these domains differ, the browser blocks the response by default. CORS is the explicit mechanism that allows a server to 'opt-in' to relaxing these strict constraints, creating a bridge between trusted domains while maintaining security for others.
// Browsers use the origin header to verify requests.
// A request from http://frontend.com to http://api.com
// will be blocked unless the API explicitly permits it.
fetch('https://api.example.com/data', {
method: 'GET'
}).catch(err => console.error('Blocked by SOP:', err));Handling Simple Requests
When a frontend makes a 'simple' request—typically a standard GET, POST, or HEAD request with limited header types—the browser performs a direct communication flow. The browser sends the request with an 'Origin' header identifying where the script is running. The server checks this origin against its internal whitelist. If allowed, the server responds with the 'Access-Control-Allow-Origin' header, explicitly echoing back the permitted domain. If the server does not include this header, the browser intercepts the incoming response at the network layer and denies access to the JavaScript code, ensuring the data never reaches the application logic. This mechanism effectively delegates the decision-making process to the API developer, who defines which specific origins are allowed to read the response data, thereby protecting the API from being exploited by arbitrary, untrusted frontend scripts.
// Server-side response header configuration:
// Express.js example for simple origins
app.get('/public', (req, res) => {
// Allow only the specific frontend to access this resource
res.setHeader('Access-Control-Allow-Origin', 'https://frontend.com');
res.json({ data: 'This is public API content.' });
});The Preflight Mechanism
Not all requests are simple; those involving custom headers, content types like 'application/json', or methods like PUT or DELETE trigger a preflight request. Before the actual request is sent, the browser automatically dispatches an HTTP OPTIONS request to the server. This serves as a safety check, asking the server if it accepts the upcoming operation before the actual payload is transmitted. The server must respond to this OPTIONS request with specific headers: 'Access-Control-Allow-Methods' and 'Access-Control-Allow-Headers', which detail exactly what the client is permitted to do. The browser then examines these headers; if the actual request parameters match the server's pre-approved list, the browser proceeds with the real request. This two-step dance prevents potentially destructive actions—like deleting database entries—from occurring without the server's explicit validation and acknowledgement of the cross-origin caller's intentions.
// Handling the OPTIONS preflight request
app.options('/data', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', 'https://frontend.com');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.sendStatus(204); // No content needed for preflight
});Managing Credentials and Cookies
In many scenarios, you need to share authentication state, such as HTTP-only cookies or authorization headers, across origins. By default, CORS requests do not include user credentials for security reasons. To override this, the frontend must set the 'withCredentials' flag to true on its request object. However, this action imposes strict server-side requirements: the server must respond with the header 'Access-Control-Allow-Credentials: true'. Furthermore, in this specific mode, the server cannot use a wildcard ('*') for the 'Access-Control-Allow-Origin' header. It must return a specific, single origin that matches the request. This design forces developers to be explicit about exactly which domains can access authenticated content, preventing misconfiguration where an API might accidentally expose sensitive, user-specific data to any requesting site on the internet by using an overly permissive wildcard policy.
// Frontend code to send credentials
fetch('https://api.example.com/user', {
method: 'GET',
credentials: 'include' // Required for cookies/auth headers
});
// Server must be specific with origins when using credentials
res.setHeader('Access-Control-Allow-Origin', 'https://trusted-app.com');
res.setHeader('Access-Control-Allow-Credentials', 'true');Best Practices for Security
Implementing CORS requires a balance between accessibility and security. The most critical rule is to avoid the wildcard '*' header in production environments, especially when credentials are involved, as it renders your CORS policy effectively useless by allowing any origin to interact with your API. Instead, maintain a whitelist of allowed domains and implement a function to check the incoming 'Origin' header against this list dynamically. If the incoming request matches a trusted entry, mirror that origin back in the 'Access-Control-Allow-Origin' header. Additionally, ensure that your preflight OPTIONS logic is robust, caching the results using 'Access-Control-Max-Age' to reduce network overhead and latency for subsequent requests. By treating CORS as a strictly governed whitelist rather than a convenience feature, you ensure that your API remains protected from unauthorized cross-site scripting and unauthorized data access while providing necessary flexibility for legitimate client integrations.
// Dynamic whitelist implementation
const allowedOrigins = ['https://app.com', 'https://admin.com'];
app.use((req, res, next) => {
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Max-Age', '86400'); // Cache for 24h
}
next();
});Key points
- CORS is a browser-side security feature that manages cross-origin resource access.
- The Same-Origin Policy is the default restriction that prevents unauthorized cross-domain data access.
- A simple request involves a single HTTP exchange with restricted methods and headers.
- Preflight requests use the OPTIONS method to verify if a cross-origin operation is permitted.
- The Access-Control-Allow-Origin header is the primary mechanism for authorizing specific domains.
- Wildcard values for origins are dangerous and should be avoided in production environments.
- Using credentials in CORS requests requires explicit server-side consent and specific origin responses.
- Dynamic whitelist checks provide a secure and flexible way to manage multi-origin access.
Common mistakes
- Mistake: Using '*' in the Access-Control-Allow-Origin header while requiring credentials. Why it's wrong: Browsers block requests that combine wildcard origins with cookies or authorization headers for security. Fix: Echo the requesting origin dynamically based on an allowlist.
- Mistake: Failing to handle the OPTIONS preflight request. Why it's wrong: Browsers send a preflight request for non-simple methods, and if the server doesn't return a 204 or 200, the actual request is never made. Fix: Implement an OPTIONS route handler that returns the appropriate CORS headers.
- Mistake: Including multiple domains in the Access-Control-Allow-Origin header. Why it's wrong: The specification only allows a single origin or the wildcard character. Fix: Dynamically validate the 'Origin' header from the request and reflect it back in the response if it is trusted.
- Mistake: Forgetting to whitelist custom headers in Access-Control-Allow-Headers. Why it's wrong: Custom headers (like X-API-KEY) are not included in simple requests and require explicit preflight permission. Fix: Ensure all custom headers used by the client are listed in the Access-Control-Allow-Headers header.
- Mistake: Expecting CORS to provide server-side security. Why it's wrong: CORS is a browser-side mechanism to prevent malicious scripts from reading data, not an API authentication method. Fix: Use standard authentication headers and tokens for actual data protection.
Interview questions
What is the primary purpose of CORS in the context of REST API design?
CORS, or Cross-Origin Resource Sharing, is a browser-based security mechanism that allows a server to explicitly indicate which origins are permitted to access its resources. In REST API design, it is essential because browsers enforce the Same-Origin Policy, which restricts scripts on one domain from making requests to another. CORS allows us to safely bypass this restriction by adding specific HTTP headers like 'Access-Control-Allow-Origin', ensuring that APIs remain secure while still allowing legitimate cross-domain data consumption by client applications.
How does the CORS preflight request work, and why is it triggered?
A preflight request is an automatic 'OPTIONS' request sent by the browser before the actual data-changing request is made. It is triggered when the request is considered 'non-simple', meaning it uses methods like PUT, DELETE, or custom headers. The purpose is to ask the server for permission before sending sensitive data. The server responds with headers indicating if the requested method and headers are allowed, protecting the API from unauthorized actions by confirming the client’s intentions.
What is the difference between an 'Access-Control-Allow-Origin' header value of '*' versus a specific domain?
Setting 'Access-Control-Allow-Origin' to '*' makes the API public, allowing any domain to read the response. This is often used for open, public data sets. However, if your API requires authentication or uses credentials, you cannot use '*' as the value; you must define the specific, trusted origin. Using a specific domain is significantly more secure because it limits the blast radius and prevents malicious third-party websites from making authenticated requests on behalf of your users.
Compare the 'Access-Control-Allow-Credentials' header approach to using a proxy server to handle cross-origin requests.
Using 'Access-Control-Allow-Credentials' allows the browser to send cookies or Authorization headers alongside the cross-origin request, which is necessary for session-based APIs. Conversely, a proxy server acts as a middleman that sits on the same origin as the client, fetching data from the API and passing it back. While credentials allow direct communication, a proxy completely bypasses CORS restrictions at the browser level, which can be useful when you lack control over the API server's response headers.
Why is it considered a security risk to reflect the 'Origin' request header directly into the 'Access-Control-Allow-Origin' response header?
Simply echoing the 'Origin' header back to the client effectively disables your CORS policy entirely, making your API as vulnerable as if you had set the policy to '*'. An attacker could host a malicious site and, because your server blindly trusts any incoming origin, the browser would allow the malicious site to read your API data. A secure design requires maintaining a strict allow-list of known, trusted domains on your server to validate the 'Origin' header before reflecting it.
How would you design a robust CORS policy for a REST API that supports both public read-only endpoints and private, authenticated endpoints?
I would implement a middleware layer that inspects the request path and the incoming 'Origin'. For public endpoints, I would permit a wider range of origins or even '*'. For authenticated endpoints, I would enforce strict origin matching against a database of allowed domains, ensuring 'Access-Control-Allow-Credentials' is only returned if the origin is explicitly trusted. I would also ensure the server responds with appropriate 'Vary: Origin' headers to prevent caching issues, ensuring that the browser correctly caches CORS responses based on the specific origin requesting the resource.
Check yourself
1. A browser performs a preflight request. What is the primary purpose of this communication?
- A.To perform a security handshake with the server's TLS certificate.
- B.To check if the server allows the specific HTTP method and headers of the intended request.
- C.To retrieve a CSRF token for the subsequent request.
- D.To authenticate the user before allowing the cross-origin fetch.
Show answer
B. To check if the server allows the specific HTTP method and headers of the intended request.
The preflight request determines if the server permits the actual cross-origin request. TLS handshakes are separate; CSRF tokens are part of application logic; and authentication happens during the actual request, not the preflight.
2. When can a browser omit the OPTIONS preflight request for a cross-origin call?
- A.When the request is a simple GET request using standard content types.
- B.When the origin is part of a public domain registry.
- C.When the user is logged into the browser.
- D.When the request uses the HTTPS protocol.
Show answer
A. When the request is a simple GET request using standard content types.
Simple requests (GET/HEAD/POST with specific media types) do not trigger a preflight. The others are incorrect because protocol or login status do not bypass the CORS spec, and public registries are irrelevant.
3. If your API needs to support cookies in a cross-origin request, what must be true?
- A.The server must set Access-Control-Allow-Origin to '*'.
- B.The client must set the 'withCredentials' property to true and the server must return 'Access-Control-Allow-Credentials: true'.
- C.The API must only support POST requests.
- D.The browser will automatically bypass the origin check if cookies are present.
Show answer
B. The client must set the 'withCredentials' property to true and the server must return 'Access-Control-Allow-Credentials: true'.
Credentialed requests require explicit opt-in from both sides. Wildcards are forbidden with credentials, POST is not required, and browsers never bypass origin checks.
4. What happens if a server omits the 'Access-Control-Allow-Origin' header in its response to a request?
- A.The browser defaults to allowing the request.
- B.The browser permits the request but logs a warning.
- C.The browser blocks the response from the client-side code, resulting in an error.
- D.The server is treated as a same-origin resource.
Show answer
C. The browser blocks the response from the client-side code, resulting in an error.
CORS is a browser-enforced security model. If the header is missing, the browser blocks access to the response. It does not default to allowing it, warn only, or change the resource's origin status.
5. An API client sends a custom header 'X-Requested-By'. What must the server do to allow this request?
- A.Include 'X-Requested-By' in the 'Access-Control-Allow-Headers' response header.
- B.Accept the header automatically because it is a custom field.
- C.Use the 'Access-Control-Allow-Methods' header to register the header name.
- D.Ignore the header since it is not a standard HTTP field.
Show answer
A. Include 'X-Requested-By' in the 'Access-Control-Allow-Headers' response header.
Custom headers must be explicitly whitelisted in the preflight response headers. Simply ignoring or using the wrong header field will result in a blocked request.