What the same-origin policy and CORS do
The same-origin policy stops a script on one origin from reading responses or page data from another origin. CORS (Cross-Origin Resource Sharing) is how a server tells the browser that specific other origins may read its responses. The browser enforces both.
An origin is the combination of scheme, host and port; the path does not count. Compared with https://app.example.com:
- https://app.example.com/settings is the same origin, because only the path differs.
- http://app.example.com is a different origin, because the scheme differs.
- https://api.example.com is a different origin, because the host differs.
- https://app.example.com:8443 is a different origin, because the port differs.
What the same-origin policy blocks and allows
The policy mostly restricts reading. A script on another site can't use fetch to read your API's JSON or read the content of a cross-origin iframe. It also can't read the response to a form it posts across origins.
Sending is a different matter. Links, form submissions, images, scripts and stylesheets can all make cross-origin requests, and the browser attaches cookies according to their SameSite setting. That gap is why cross-site request forgery exists, and why servers need their own defenses against it.
How CORS relaxes the policy
When a script makes a cross-origin request, the browser adds an Origin header. If the response carries Access-Control-Allow-Origin with that exact origin, or *, the script may read the response. Otherwise the request may still reach the server, but the script gets a CORS error instead of the data.
If the allowed origin depends on the request, also send Vary: Origin so shared caches don't serve one origin's response to another.
GET /v1/profile HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Vary: Origin
Content-Type: application/jsonPreflight requests
For requests a plain HTML form couldn't send, the browser asks first with an OPTIONS request called a preflight. It carries Access-Control-Request-Method and Access-Control-Request-Headers, and the real request goes out only if the response approves them.
The server approves with Access-Control-Allow-Origin, Access-Control-Allow-Methods and Access-Control-Allow-Headers. An optional Access-Control-Max-Age allows the browser to cache the approval for a while.
Any of these triggers a preflight:
- A method other than GET, HEAD or POST, such as PUT or DELETE.
- A request header outside the CORS safelist, such as Authorization or a custom X- header.
- A Content-Type other than application/x-www-form-urlencoded, multipart/form-data or text/plain, which means a JSON body triggers one.
Credentials and the wildcard rule
By default, cross-origin fetch requests carry no cookies. A script must opt in with credentials: 'include', and the server must reply with Access-Control-Allow-Credentials: true and an explicit origin.
Browsers reject Access-Control-Allow-Origin: * on a credentialed request, because a wildcard plus cookies would expose a signed-in user's data to any website. Echoing back whatever Origin header arrives recreates that hole, so compare the origin against an allowlist of exact values.
CORS doesn't replace server-side access control
CORS only decides which browser scripts may read a response. It doesn't stop anyone from calling your API, because curl, server-side code and an attacker's own tools ignore CORS headers.
Protect every endpoint with authentication and authorization on the server. A restrictive CORS policy does not make an unauthenticated endpoint private.