Last updated: Sep 9, 2026

CORS

Cross-Origin Resource Sharing (CORS) is a browser mechanism that lets a server opt in to relaxing the Same-Origin Policy for a specific origin. The browser adds an Origin header to cross-origin requests, and JavaScript can read the response only when the server replies with matching Access-Control-* headers.

Only the browser enforces this and it’s not CSRF protection.

Same-Origin Policy

By default a page at https://app.example.com cannot read a response from https://api.example.com. CORS does not replace that rule; the server opts in by naming the allowed origin (or *) in Access-Control-Allow-Origin.

Same-Origin Policy still allows some cross-origin writes without CORS — for example a form POST. That is why CSRF exists. CORS is the mechanism that, if misconfigured (Access-Control-Allow-Origin reflecting any Origin plus credentials), can also expose response bodies to another origin.

Simple vs preflight

A simple request (CORS-safelisted) is sent immediately. The browser still hides the response from JavaScript unless the response includes a matching Access-Control-Allow-Origin. The request itself already reached the server.

A request is simple when all of these hold:

  • Method is GET, HEAD, or POST
  • Headers are only CORS-safelisted request headers (Accept, Accept-Language, Content-Language, and Content-Type)
  • Content-Type, if present, is application/x-www-form-urlencoded, multipart/form-data, or text/plain

Anything else is preflighted. The browser first sends OPTIONS (shown as “Preflight” in DevTools). The actual request goes out only if that OPTIONS response allows it. Authorization, application/json, PUT, PATCH, and DELETE all trigger a preflight.

Preflight request headers

The browser sets these on the OPTIONS request to describe the actual request:

HeaderRole
OriginThe page’s origin
Access-Control-Request-MethodMethod of the actual request
Access-Control-Request-HeadersNon-safelisted headers the actual request will send

Response headers

The server sets these on the preflight response, and repeats Access-Control-Allow-Origin (and credentials, if used) on the actual response:

HeaderRole
Access-Control-Allow-OriginA single origin, or *. Not a list.
Access-Control-Allow-MethodsMethods allowed for the actual request
Access-Control-Allow-HeadersHeaders allowed on the actual request (answers Access-Control-Request-Headers)
Access-Control-Allow-Credentialstrue if cookies or HTTP auth may be included
Access-Control-Max-AgeSeconds the browser may cache this preflight
Access-Control-Expose-HeadersResponse headers JavaScript may read, beyond the safelisted set (Cache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified, Pragma)

Allow-Headers and Expose-Headers are different allowlists: one for request headers the client wants to send, one for response headers the client wants to read.

To allow more than one origin, echo a known Origin back as Access-Control-Allow-Origin instead of listing several values.

When that value depends on the request Origin, send Vary: Origin so caches do not reuse one origin’s response for another.

Credentials and *

Credentialed requests (fetch(..., { credentials: "include" }), or XHR withCredentials) send cookies and HTTP auth. The response must use a specific origin, not *:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true

Access-Control-Allow-Origin: * together with Access-Control-Allow-Credentials: true is rejected by the browser. A Set-Cookie on a response whose Access-Control-Allow-Origin is * also does not set a cookie.

no-cors and proxies

fetch(url, { mode: "no-cors" }) still sends the request, but the result is an opaque response: JavaScript cannot read the status, body, or headers. It does not bypass SOP.

Simple request example

Page at https://app.example.com POSTs a form to https://api.example.com/data. Content-Type is application/x-www-form-urlencoded.

POST /data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Content-Type: application/x-www-form-urlencoded

name=Ada
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json

{"ok": true}

Without Access-Control-Allow-Origin, this POST still hits the server; JavaScript just cannot read the JSON.

Preflighted request

Page at https://app.example.com POSTs JSON to https://api.example.com/users with a cookie.

OPTIONS /users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: content-type
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400
Vary: Origin

Then the actual request:

POST /users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Content-Type: application/json
Cookie: session=abc

{"name": "Ada"}
HTTP/1.1 201 Created
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Vary: Origin
Content-Type: application/json

{"id": "1"}