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, orPOST - Headers are only CORS-safelisted request headers (
Accept,Accept-Language,Content-Language, andContent-Type) Content-Type, if present, isapplication/x-www-form-urlencoded,multipart/form-data, ortext/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:
| Header | Role |
|---|---|
Origin | The page’s origin |
Access-Control-Request-Method | Method of the actual request |
Access-Control-Request-Headers | Non-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:
| Header | Role |
|---|---|
Access-Control-Allow-Origin | A single origin, or *. Not a list. |
Access-Control-Allow-Methods | Methods allowed for the actual request |
Access-Control-Allow-Headers | Headers allowed on the actual request (answers Access-Control-Request-Headers) |
Access-Control-Allow-Credentials | true if cookies or HTTP auth may be included |
Access-Control-Max-Age | Seconds the browser may cache this preflight |
Access-Control-Expose-Headers | Response 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"}