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, and Range
  • Content-Type (if present) is application/x-www-form-urlencoded, multipart/form-data, or text/plain
  • No event listeners registered on XMLHttpRequest.upload
  • No ReadableStream in 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:

Preflight flow

  1. The browser detects a cross-origin request requiring preflight
  2. The browser sends an OPTIONS request with Access-Control-Request-Method and Access-Control-Request-Headers
  3. The server responds with permission headers (Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers)
  4. If the preflight succeeds, the browser sends the actual request
  5. 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-Origin must be the exact requesting origin (* is not allowed)
  • Access-Control-Allow-Credentials: true must be present
  • Access-Control-Allow-Methods must list methods explicitly (* is not allowed)
  • Access-Control-Allow-Headers must 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

Last updated: March 6, 2026