CORS: Cross-Origin Resource Sharing Explained
Intermediate
If you have ever built a web application that calls an API hosted on a different domain, you have encountered CORS. The browser console error "Access to fetch at 'https://api.example.com' from origin 'https://www.example.com' has been blocked by CORS policy" is one of the most common frustrations in web development. But CORS is not a bug or an obstacle — it is a carefully designed security mechanism that protects users while enabling the interconnected architecture of the modern web.
This article explains CORS from its historical roots in the same-origin policy, through the mechanics of simple and preflighted requests, to practical CDN configuration and troubleshooting. Understanding CORS deeply transforms it from a source of confusion into a powerful tool for secure cross-domain communication.
What Is the Same-Origin Policy and Why Was It Created?
The same-origin policy is the foundational browser security mechanism that CORS extends. Introduced in Netscape Navigator 2.0 in 1995, it restricts how a document or script loaded from one origin can interact with resources from another origin. Two URLs share the same origin only when they have identical protocol (scheme), host (domain), and port.
Consider these examples relative to https://www.example.com:443/page:
https://www.example.com/other— Same origin (same protocol, host, port 443 implied)http://www.example.com/page— Different origin (http vs https)https://api.example.com/data— Different origin (different subdomain)https://www.example.com:8080/page— Different origin (different port)
Without the same-origin policy, the web would be fundamentally unsafe. A malicious website could use JavaScript to silently read your email from Gmail, access your bank account data, or steal session tokens from any site you are logged into. The same-origin policy prevents this by ensuring that scripts running in one origin cannot read responses from another origin.
However, the policy created a practical problem. As the web evolved from monolithic applications to distributed architectures — with APIs on separate subdomains, assets on CDNs, and third-party integrations everywhere — legitimate cross-origin requests became essential. Web fonts hosted on Google Fonts, API calls from single-page applications to backend services, and embedded payment widgets all require cross-origin access. CORS was the W3C's answer to this tension between security and functionality.
How Does CORS Work at the Protocol Level?
CORS is an HTTP header-based protocol that allows servers to declare which origins are permitted to access their resources. It was first proposed in 2004 by Matt Oshry, Brad Porter, and Michael Bodell at Tellme Networks, developed through the W3C process, and became a W3C Recommendation in January 2014. It is now maintained as part of the Fetch Living Standard by WHATWG.
| Metric | Edge Processing | Origin Processing | Improvement |
|---|---|---|---|
| Latency (p50) | 8–15 ms | 120–300 ms | 15–20x faster |
| Cache Hit Ratio | 85–95% | N/A | Reduces origin load |
| TLS Handshake | 1–5 ms (nearby PoP) | 50–150 ms | 10–30x faster |
| Bandwidth Cost | $0.01–0.04/GB | $0.05–0.12/GB | 3–5x cheaper |
| Availability | 99.99% (multi-PoP) | 99.9% (single DC) | 10x fewer outages |
The mechanism works through a conversation between the browser and the server. When JavaScript on page https://app.example.com makes a request to https://api.example.com, the browser adds an Origin header to the request. The server examines this header and, if it permits the origin, includes an Access-Control-Allow-Origin header in the response. The browser checks this response header and either delivers the response to JavaScript or blocks it.
Crucially, the server always receives the request and sends a response. CORS is enforced by the browser, not the server. The server performs its work regardless — CORS only determines whether the browser allows the client-side JavaScript to read the response. This is why CORS provides no protection against server-to-server requests (which do not involve a browser) and why server-side security measures remain essential.
What Is the Difference Between Simple and Preflighted Requests?
CORS classifies cross-origin requests into two categories based on their potential to cause side effects on the server.
Simple requests
A cross-origin request is classified as "simple" (formally, it does not trigger a preflight) when it meets all of these conditions: the method is GET, HEAD, or POST; the only headers set manually are from the CORS-safelisted set (Accept, Accept-Language, Content-Language, Content-Type); and the Content-Type is one of application/x-www-form-urlencoded, multipart/form-data, or text/plain.
For simple requests, the browser sends the request directly with an Origin header. The server includes Access-Control-Allow-Origin in the response, and the browser either delivers or blocks the response based on that header.
Preflighted requests
Any request that does not qualify as "simple" triggers a preflight — an automatic OPTIONS request sent by the browser before the actual request. The preflight asks the server: "Will you accept a PUT request with an Authorization header from this origin?" The server responds with Access-Control-Allow-Methods and Access-Control-Allow-Headers indicating what it permits. Only if the preflight succeeds does the browser send the actual request.
Common triggers for preflight requests include: using HTTP methods like PUT, DELETE, or PATCH; setting the Authorization header; sending Content-Type: application/json; or including any custom header (e.g., X-Request-ID).
Preflight responses can be cached using the Access-Control-Max-Age header. Setting this to 86400 (24 hours, the maximum Chrome allows) means the browser will not repeat the preflight for matching requests within that window. This is critical for API performance — without caching, every API call would require two HTTP requests instead of one.
What Are the Key CORS Response Headers?
The server controls CORS behavior through several response headers, each serving a specific purpose.
Access-Control-Allow-Origin is the primary header. It specifies which origin is permitted. The value can be a specific origin (https://app.example.com) or the wildcard * (any origin). The wildcard cannot be used when credentials are involved. A common pattern is to dynamically set this header to match the Origin of the incoming request, after validating it against a whitelist. When doing so, the Vary: Origin header must be included to prevent caching issues.
Access-Control-Allow-Methods lists the HTTP methods permitted for cross-origin requests (e.g., GET, POST, PUT, DELETE). This is only needed in preflight responses.
Access-Control-Allow-Headers lists the request headers permitted beyond the safelisted set (e.g., Authorization, Content-Type, X-Request-ID). Also only needed in preflight responses.
Access-Control-Allow-Credentials — when set to true, indicates that the request can include credentials (cookies, HTTP authentication, TLS client certificates). When credentials are used, Access-Control-Allow-Origin must specify the exact origin, not *. This is the most security-sensitive CORS header — setting it to true with a permissive origin whitelist effectively opens your API to cross-origin authenticated requests.
Access-Control-Expose-Headers specifies which response headers JavaScript can access. By default, only the CORS-safelisted response headers (Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma) are exposed to scripts. Custom headers like X-RateLimit-Remaining must be explicitly exposed.
How Do CDNs Handle CORS Headers?
CDN CORS configuration requires careful attention because CDNs cache responses, and CORS headers are part of those responses. The interaction between caching and CORS creates a specific problem that catches many teams off guard.
Imagine a font file cached by a CDN with Access-Control-Allow-Origin: https://site-a.com. When https://site-b.com requests the same font, the CDN serves the cached response — but the CORS header specifies only site-a.com. The browser on site-b.com rejects the response, and the font fails to load.
The solution is the Vary: Origin header. Including Vary: Origin tells the CDN (and any intermediate cache) that the response varies based on the Origin request header. The CDN maintains separate cached copies for each origin, ensuring each receives the correct CORS headers.
CDNs offer two approaches to CORS configuration. Origin passthrough forwards the CORS headers set by the origin server, caching the response keyed by the Origin header. This is simpler and keeps CORS logic centralized. Edge-side CORS injection adds CORS headers at the CDN edge, regardless of what the origin server returns. This is useful when you do not control the origin server or want consistent CORS policy across all responses, and is typically configured through CDN rules or edge functions.
When configuring security headers at the CDN edge, ensure that Access-Control-Allow-Origin is set consistently and that preflight OPTIONS responses are handled efficiently. Many CDNs can cache preflight responses at the edge, avoiding the round trip to the origin for repeated preflights.
What Are the Most Common CORS Errors and How Do You Fix Them?
CORS errors manifest in the browser console and can be difficult to debug because the browser intentionally limits the information available to JavaScript (to prevent information leakage about cross-origin resources).
"No 'Access-Control-Allow-Origin' header is present on the requested resource." The server is not returning any CORS headers. Fix: Add Access-Control-Allow-Origin to the server's response. If the server is behind a CDN, ensure the header is being passed through or injected at the edge.
"The value of the 'Access-Control-Allow-Origin' header must not be the wildcard '*' when the request's credentials mode is 'include'." You are sending cookies (credentials: 'include' in fetch) but the server returns Access-Control-Allow-Origin: *. Fix: Set the header to the specific requesting origin and add Access-Control-Allow-Credentials: true.
"Method PUT is not allowed by Access-Control-Allow-Methods in preflight response." The server's preflight response does not include the method your request uses. Fix: Add the method to Access-Control-Allow-Methods in the OPTIONS response handler.
"Request header field Authorization is not allowed by Access-Control-Allow-Headers." The server's preflight response does not list the custom header your request sends. Fix: Add Authorization (or whatever custom header) to Access-Control-Allow-Headers.
Intermittent CORS failures on CDN-cached resources. Cached responses are being served with the wrong origin. Fix: Add Vary: Origin to all CORS-enabled responses, and purge existing cached responses after the change.
How Should CORS Be Configured for Maximum Security?
CORS is a security mechanism, and misconfiguration can create serious vulnerabilities. The most dangerous misconfiguration is reflecting the Origin header value into Access-Control-Allow-Origin with Access-Control-Allow-Credentials: true — this effectively allows any website on the internet to make authenticated requests to your API and read the responses, defeating the purpose of the same-origin policy entirely.
Follow these security principles. Whitelist specific origins rather than using the wildcard. Maintain an explicit list of permitted origins and validate incoming Origin headers against it. Avoid reflecting the origin blindly — always validate it against your whitelist before echoing it back. Limit exposed methods and headers to only what the application actually needs. Be cautious with credentials — only enable Access-Control-Allow-Credentials: true when cross-origin cookie/auth access is genuinely required. Use Content Security Policy (CSP) as a complementary defense that controls what resources a page can load, providing defense-in-depth alongside CORS.
For public APIs and CDN-hosted assets (fonts, images, scripts) that any website should be able to load, the wildcard Access-Control-Allow-Origin: * is appropriate and secure, as long as credentials are not involved. The risk is limited to information that is already public.
How Does CORS Interact with Other Security Headers?
CORS works alongside other HTTP security mechanisms. Content Security Policy (CSP) controls what a page can load, while CORS controls what a server allows to be loaded cross-origin — they protect from opposite directions. Strict-Transport-Security (HSTS) ensures HTTPS, which is important because CORS considers http:// and https:// as different origins. X-Content-Type-Options: nosniff prevents browsers from MIME-type sniffing, which could bypass CORS in certain edge cases.
A well-configured CDN security header policy layers these protections together, with CORS handling cross-origin access control, CSP preventing injection attacks, HSTS enforcing transport security, and a Web Application Firewall providing request-level filtering.