Cross-Origin Resource Sharing (CORS)
Cross-Origin Resource Sharing (CORS) is an HTTP-header-based mechanism allowing a server to declare which Origins are permitted to read its responses through a browser. CORS extends the same-origin policy, the browser's default deny rule preventing scripts on one origin from accessing resources on a different origin.
An origin is defined by the combination
of scheme, host, and port. https://app.example.re
and http://app.example.re are different origins
because the scheme differs. https://app.example.re
and https://api.example.re are different origins
because the host differs.
CORS is enforced by the browser, not the server. The server declares permissions through response headers. The browser checks those headers and either exposes or blocks the response to JavaScript. The request itself often reaches the server regardless. CORS controls whether the browser allows the calling script to read the response.
Baseline: Widely available
Supported in all major browsers. webstatus.dev
Same-origin policy
The same-origin policy (SOP) was introduced in Netscape Navigator 2.0 in 1995 alongside JavaScript. The SOP prevents scripts running on one origin from reading data returned by a different origin. Without this restriction, a malicious page exploits the visitor's authenticated session to silently read email, banking data, or internal corporate resources.
The SOP is the default deny. CORS is the controlled exception mechanism. Servers opt in to cross-origin access by sending specific response headers. No CORS headers means the browser blocks cross-origin script access.
Simple requests
Not all cross-origin requests trigger a preflight. A request avoids preflight when all of the following conditions are met:
- Method is GET, HEAD, or POST
- Headers are limited to CORS-safelisted request
headers:
Accept,Accept-Language,Content-Language, Content-Type, andRange - Content-Type (if present) is
application/x-www-form-urlencoded,multipart/form-data, ortext/plain - No event listeners registered on
XMLHttpRequest.upload - No
ReadableStreamin the request body
A request meeting these conditions is sent directly. The browser adds the Origin header and checks the response for Access-Control-Allow-Origin.
Preflight requests
Any cross-origin request not meeting the simple request conditions triggers a preflight, an OPTIONS request sent before the actual request to check whether the server allows the actual request.
Common preflight triggers:
- Methods other than GET, HEAD, POST (e.g., PUT, DELETE, PATCH)
- Non-safelisted headers (e.g.,
Authorization, custom headers
like
X-API-Key) - Content-Type values like
application/jsonortext/xml
Preflight flow
- The browser detects a cross-origin request requiring preflight
- The browser sends an OPTIONS request with Access-Control-Request-Method and Access-Control-Request-Headers
- The server responds with permission headers (Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers)
- If the preflight succeeds, the browser sends the actual request
- If the preflight fails, the browser blocks the actual request entirely (the request is never sent)
Preflight request
OPTIONS /api/data HTTP/1.1
Host: api.example.re
Origin: https://app.example.re
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization
Preflight response
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.re
Access-Control-Allow-Methods: GET, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Actual request (sent after successful preflight)
PUT /api/data HTTP/1.1
Host: api.example.re
Origin: https://app.example.re
Authorization: Bearer eyJhbGciOi...
Content-Type: application/json
{"name": "updated"}
Actual response
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.re
Content-Type: application/json
{"status": "ok"}
Preflight caching
The browser maintains a preflight cache separate from the HTTP cache. The Access-Control-Max-Age header controls how long the preflight result is cached, keyed by method and URL.
| Browser | Maximum cache duration |
|---|---|
| Firefox | 86,400 seconds (24 hours) |
| Chromium-based | 7,200 seconds (2 hours) |
| Safari | ~300 seconds (5 minutes) |
When no Access-Control-Max-Age header is present,
the default cache duration is 5 seconds.
CORS request headers
The browser sends these headers automatically. JavaScript cannot set or modify them. They are forbidden request headers.
Origin
The Origin header identifies the requesting origin (scheme, host, and port). Sent on all cross-origin requests and on same-origin POST, PUT, and DELETE requests.
Access-Control-Request-Method
Access-Control-Request-Method declares which HTTP method the actual request intends to use. Sent only in preflight (OPTIONS) requests.
Access-Control-Request-Headers
Access-Control-Request-Headers lists the non-safelisted headers the actual request intends to include. Sent only in preflight requests.
CORS response headers
The server sends these headers to declare cross-origin permissions.
Access-Control-Allow-Origin
Access-Control-Allow-Origin
specifies which origin is allowed to read the
response. Accepts a single origin value
(e.g., https://app.example.re) or the wildcard
*. Only one value is permitted per response. A
comma-separated list of origins is not valid.
The wildcard * allows any origin but is not
compatible with credentialed requests. Partial
wildcards like *.example.re are not supported.
To support multiple origins, the server checks the
Origin request header against an
allowlist, reflects the matching origin in the
response, and includes Vary: Origin to prevent
Caching issues.
Access-Control-Allow-Methods
Access-Control-Allow-Methods
lists the HTTP methods the server accepts for
cross-origin requests. Returned in preflight
responses. The wildcard * is allowed for
non-credentialed requests.
Access-Control-Allow-Headers
Access-Control-Allow-Headers
lists the headers the server accepts in cross-origin
requests. Returned in preflight responses. The
wildcard * is allowed for non-credentialed
requests.
Access-Control-Expose-Headers
Access-Control-Expose-Headers controls which response headers a script reads. Without this header, scripts access only the CORS-safelisted response headers: Cache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified, and Pragma.
Access-Control-Max-Age
Access-Control-Max-Age specifies how many seconds the preflight result is cached. Browsers enforce their own caps and ignore values exceeding the limit.
Access-Control-Allow-Credentials
Access-Control-Allow-Credentials
set to true signals the browser to expose the
response when credentials were sent. Required
on both the preflight and the actual response for
credentialed requests.
Credentialed requests
Credentials include Cookies, TLS client certificates, and HTTP Authentication entries. By default, cross-origin requests do not include credentials.
To send credentials:
- Fetch API:
fetch(url, { credentials: "include" }) - XMLHttpRequest:
xhr.withCredentials = true
When credentials are included, the server must meet all of the following conditions:
Access-Control-Allow-Originmust be the exact requesting origin (*is not allowed)Access-Control-Allow-Credentials: truemust be presentAccess-Control-Allow-Methodsmust list methods explicitly (*is not allowed)Access-Control-Allow-Headersmust list headers explicitly (*is not allowed)
The preflight OPTIONS request itself
never includes credentials (no Cookies are
sent with OPTIONS). The preflight response must
still include
Access-Control-Allow-Credentials: true for the
actual credentialed request to proceed.
Third-party cookie policies
Browser third-party cookie policies and the
SameSite cookie attribute override CORS
permissions. A Set-Cookie in a CORS response
has no effect when
Access-Control-Allow-Origin: * is used.
CORS by resource type
Not all cross-origin requests use CORS by default.
HTML elements load cross-origin resources in
no-cors mode unless opted in.
| Element | Default mode | CORS required? |
|---|---|---|
<img> |
no-cors | No |
<script> |
no-cors | No |
<link> (stylesheet) |
no-cors | No |
<audio>, <video> |
no-cors | No |
fetch() / XMLHttpRequest |
cors | Yes |
@font-face |
cors | Always |
| WebGL textures | cors | Yes |
<canvas> data extraction |
cors | Yes |
The crossorigin attribute
Adding crossorigin to an HTML element switches
its fetch mode from no-cors to cors:
| Value | Credentials mode |
|---|---|
anonymous (or empty string) |
same-origin |
use-credentials |
include |
Fonts always require CORS
The CSS Fonts specification mandates CORS for all
cross-origin font loads. A @font-face rule
fetching from a different origin fails silently
without proper CORS headers. When preloading fonts
via <link rel="preload" as="font">, the
crossorigin attribute must be present, even for
same-origin fonts.
Canvas taint
Drawing a cross-origin image (loaded without CORS)
onto a <canvas> taints the canvas. A tainted
canvas blocks getImageData(), toBlob(),
toDataURL(), and captureStream() with a
SecurityError. To avoid taint: set
crossorigin="anonymous" on the <img> element
and the server must respond with CORS headers.
Script error reporting
Without crossorigin on a <script> tag,
cross-origin script errors are sanitized to
"Script error." with no line or column
information. Adding crossorigin="anonymous" (with
server CORS support) enables full error details in
window.onerror.
no-cors mode
The Fetch API supports no-cors mode:
fetch(url, { mode: "no-cors" }). This restricts
the request to GET, HEAD, and POST with only
safelisted headers, and returns an opaque
response: response.type is "opaque",
response.status is 0, and the body is
inaccessible to JavaScript.
Opaque responses are useful for service workers caching third-party resources for offline use and for analytics beacons where the response content is irrelevant.
Caching and Vary
When a server dynamically reflects the requesting
origin in
Access-Control-Allow-Origin,
the response must include Vary: Origin. Without
the Vary header, a CDN or proxy caches the
response with one origin's permission header and
serves the cached response to a different origin,
causing CORS failures for the
second origin. This is a form of cache poisoning.
Security
CORS is not server-side access control
CORS is enforced by browsers only. Non-browser clients (curl, Postman, server-to-server requests) ignore CORS headers entirely. CORS does not replace Authentication or Authorization.
Common misconfigurations
Reflecting the Origin header without validation.
A server blindly copying the Origin request
header into Access-Control-Allow-Origin combined
with Access-Control-Allow-Credentials: true allows
any attacker site to read authenticated responses.
This is equivalent to disabling the same-origin
policy.
Allowing null origin with credentials.
Access-Control-Allow-Origin: null combined with
credentials is exploitable. Sandboxed iframes and
data: URIs produce a null origin, allowing an
attacker's script to match.
Partial origin matching. A server matching
example.re without proper anchoring accepts
example.re.attacker.com. Regex-based origin
validation requires exact matching with anchored
patterns.
Trusting XSS-vulnerable subdomains. If
app.example.re trusts cdn.example.re via CORS,
and cdn.example.re has an XSS vulnerability, the
attacker uses XSS on the subdomain to make
credentialed CORS requests and exfiltrate data.
Wildcard on internal resources.
Access-Control-Allow-Origin: * on intranet
services enables an external attacker to use an
employee's browser as a proxy to read internal
resources.
Private Network Access
Private Network Access (formerly CORS-RFC1918)
extends CORS to protect local and private network
resources from requests initiated by public
websites. A request from a public origin to a
private IP address (10.x, 172.16.x, 192.168.x)
or localhost requires a preflight with the
Access-Control-Request-Private-Network: true
header, and the server must respond with
Access-Control-Allow-Private-Network: true.
Chromium-based browsers enforce this. Firefox and
Safari do not yet enforce Private Network Access.
Debugging CORS
Browser console
JavaScript cannot access CORS error details.
fetch() rejects with a generic TypeError and
XMLHttpRequest fires an error event with no
useful information. This is intentional. Exposing
the failure reason leaks information about the target
server's configuration. The specific CORS error
reason appears only in the browser's developer
console.
Chrome reports CORS errors as: "Access to fetch at 'URL' from origin 'ORIGIN' has been blocked by CORS policy:" followed by the specific reason.
DevTools Network panel
Chrome DevTools displays CORS-related information
in the Network panel. Response headers show the
CORS headers the server returned. For failed
requests, the Console tab shows the specific
CORS violation. Chrome also supports overriding
response headers locally in the Network panel:
hover over a header value, click the edit icon,
and change the value. This allows prototyping
CORS fixes without server access, useful for
testing
Access-Control-Allow-Origin,
Access-Control-Allow-Headers, and other CORS
headers before deploying server-side changes.
Common errors and causes
| Error | Cause |
|---|---|
Access-Control-Allow-Origin missing |
Server sends no CORS headers |
| Origin mismatch | Server returns a different origin than the requester |
| Wildcard with credentials | * used with Access-Control-Allow-Credentials: true |
| Method not allowed | Preflight response does not list the requested method |
| Header not allowed | Preflight response does not list a requested header |
| Preflight failed | OPTIONS request returned non-2xx or network error |
Multiple Access-Control-Allow-Origin headers |
Double proxy or middleware adding duplicate headers |
curl for testing
Test CORS responses from the command line:
curl -I -H "Origin: https://app.example.re" \
https://api.example.re/data
Check for Access-Control-Allow-Origin in the
response. For preflight testing:
curl -X OPTIONS \
-H "Origin: https://app.example.re" \
-H "Access-Control-Request-Method: PUT" \
-H "Access-Control-Request-Headers: Authorization" \
-I https://api.example.re/data
Specification history
CORS was originally a standalone W3C specification, published as a W3C Recommendation in January 2014. In August 2017, the W3C specification was obsoleted because the W3C spec no longer described what browsers implemented. CORS is now defined entirely within the WHATWG Fetch Living Standard.
The Fetch standard expanded CORS with no-cors
mode, opaque responses, service worker integration,
wildcard support for
Access-Control-Allow-Methods and
Access-Control-Allow-Headers (the W3C spec only
supported * for
Access-Control-Allow-Origin), and Private Network
Access extensions.
Takeaway
CORS is the browser's mechanism for controlled exceptions to the same-origin policy. Servers declare cross-origin permissions through response headers. The browser enforces those permissions by exposing or blocking responses to JavaScript. The preflight mechanism ensures the server explicitly consents before non-simple cross-origin requests are sent. Misconfigured CORS headers, especially reflecting origins without validation or combining wildcards with credentials, create serious security vulnerabilities.
See also
- WHATWG Fetch Standard
- Access-Control-Allow-Origin
- Access-Control-Allow-Methods
- Access-Control-Allow-Headers
- Access-Control-Allow-Credentials
- Access-Control-Expose-Headers
- Access-Control-Max-Age
- Access-Control-Request-Method
- Access-Control-Request-Headers
- Origin
- OPTIONS
- Origins
- HTTP status codes
- HTTP headers