Cross-Origin Resource Sharing (CORS)
Browsers block cross-origin script access to responses by default. Cross-Origin Resource Sharing (CORS) is the HTTP-header-based mechanism allowing a server to declare which Origins are permitted to read its responses through a browser, providing controlled exceptions to this same-origin policy.
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.
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 origin, URL, method, header names, and credentials mode.
| Browser | Maximum cache duration |
|---|---|
| Firefox | 86,400 seconds (24 hours) |
| Chromium-based | 7,200 seconds (2 hours) |
| Safari | 600 seconds (10 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 requests using any method other than GET or HEAD.
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 clamp a value above their cap down to that cap rather than discarding it, so an oversized max-age still caches for the browser maximum.
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.
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.
CORS and redirects
A redirect interacts with CORS at two points, and both produce failures teams rarely expect.
A preflight response needs a status in the 200 to 299 range. A redirect status fails the requirement, so a 301 or 307 answering an OPTIONS preflight fails the whole fetch as a network error before the actual request is sent. Endpoints receiving preflights answer directly instead of redirecting to a canonical URL.
On the actual request, the browser runs the CORS check
against the redirect response itself before following
the Location target. A 302 hop lacking
Access-Control-Allow-Origin
fails the fetch even when
the final destination sends the header correctly, so
every hop in a redirect chain needs valid CORS headers.
After a cross-origin hop the browser also stops
vouching for the requesting page. The Origin
header on the follow-up request becomes null, and the
Authorization header is dropped. The
destination then sees Origin: null, the same origin
sandboxed iframes and data: URLs produce, so
answering with Access-Control-Allow-Origin: null
widens access far beyond the redirecting site. Keeping
CORS endpoints redirect-free, pointing clients at the
final URL directly, avoids the whole class of failures.
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 access control.
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.
Local Network Access
Local Network Access (LNA) protects local and
private network resources from requests initiated
by public websites. It replaces the earlier
Private Network Access proposal (formerly
CORS-RFC1918), which was put on hold. Instead of a
CORS preflight, LNA gates a request from a public
site to a private IP address (10.x, 172.16.x,
192.168.x) or localhost behind a user permission
prompt, and adds local-network and
loopback-network Permissions Policies, with
local-network-access kept as a Chromium-supported
alias. Chromium-based browsers enforce LNA.
Server configuration
CORS failures traced to the server usually come from header configuration rather than application logic. nginx configuration carries four specific traps.
Header inheritance. add_header directives are
inherited from the enclosing level only when the
current level defines none of its own. A location
block adding a single header of its own silently drops
every CORS header configured at the server level, so
each level either repeats the full set or enables
merging with add_header_inherit merge;, available
from nginx 1.29.3.
Error responses. Without the always parameter,
nginx adds headers only to a fixed list of success and
redirect status codes. A request failing with 500
or 404 then carries no CORS headers, and the
browser reports a CORS error masking the real failure.
Ending each add_header line with always keeps the
headers on error responses.
The OPTIONS branch. Applications behind nginx often lack an OPTIONS handler, so configuration answers the preflight:
location /api/ {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin "https://app.example.re";
add_header Access-Control-Allow-Methods "GET, POST, PUT";
add_header Access-Control-Allow-Headers "Authorization, Content-Type";
add_header Access-Control-Max-Age "86400";
return 204;
}
add_header Access-Control-Allow-Origin "https://app.example.re" always;
proxy_pass http://backend;
}
The if block forms its own configuration level, so
the headers repeat inside the branch by necessity. A
204 answers the preflight, and 200 works
equally well, the requirement being a status in the
200 to 299 range.
Duplicate headers. add_header appends rather than
replaces. An upstream application already sending
Access-Control-Allow-Origin plus an nginx add_header
produces the header twice, the browser reads the pair
as one comma-joined value, and the origin comparison
fails. One layer owns the CORS headers: either the
application sends them or nginx does, never both.
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 -sS -o /dev/null -D- -H "Origin: https://app.example.re" \
https://api.example.re/data
Check for Access-Control-Allow-Origin in the
response. For preflight testing:
curl -sS -o /dev/null -D- -X OPTIONS \
-H "Origin: https://app.example.re" \
-H "Access-Control-Request-Method: PUT" \
-H "Access-Control-Request-Headers: Authorization" \
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.
CORS vs CSRF
CORS controls whether a browser allows a script to read a cross-origin response. Cross-Site Request Forgery (CSRF) exploits the browser sending cross-origin requests with cookies attached.
CORS does not prevent CSRF. A cross-origin POST carrying session cookies reaches the server regardless of CORS headers. CORS only determines whether the browser exposes the response to the calling script. The request and its side effects still execute.
CSRF protection requires server-side defenses:
anti-CSRF tokens bound to the session, SameSite
cookie attributes set to Lax or Strict, and
origin validation comparing the Origin
or Referer header against an allowlist.
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