CORS Explained: Cross-Origin Resource Sharing

Easy•
Comic-style CORS flow showing a browser origin, a preflight OPTIONS request, an API server, and the browser allowing JavaScript to read the response when Access-Control-Allow-Origin matches.

CORS is best understood as a browser read-permission protocol. It sits on top of HTTP and the same-origin policy. It does not make an API private; it decides whether a browser should expose a cross-origin response to the JavaScript running inside a page.

Asked In

The Core Mental Model

Same-Origin Policy First

Browsers isolate scripts by origin. JavaScript loaded from https://app.example.com should not freely read responses from https://bank.example, https://api.other.com, or even http://app.example.com.

CORS is the server's way to say: this specific origin may read this response.

Read Access, Not Network Access

A common misunderstanding is: "CORS blocks the request."

More precise:

•The browser may still send the HTTP request.
•The server may still receive it and perform work.
•The browser decides whether JavaScript can read the response.
•If the CORS check fails, frontend code gets a generic network-style failure and the browser console has the useful details.

That is why CORS is not a replacement for server-side security.

Figure 1: CORS Boundary

Page origin: https://app.example.com

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ Browser tab                  │
│                              │
│  JavaScript fetch()          │
│        │                     │
│        │ request + Origin    │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
         │
         v
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ API server                   │
│ https://api.example.com      │
│                              │
│ Responds with:               │
│ Access-Control-Allow-Origin  │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
         │
         v
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ Browser CORS check           │
│                              │
│ Allowed? JS can read.        │
│ Denied? JS cannot read.      │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Senior phrasing: CORS is the browser asking, "Is this response allowed to enter this page's JavaScript world?"

Simple Requests

What Happens Without Preflight

Some cross-origin requests do not need a preflight. Historically, these resemble what HTML forms could already send cross-site.

A request is usually simple when it uses:

•method: GET, HEAD, or POST
•no custom request headers beyond CORS-safelisted headers
•if Content-Type is set, one of:

- application/x-www-form-urlencoded

- multipart/form-data

- text/plain

Example:

fetch('https://api.example.com/products')

The browser sends:

GET /products HTTP/1.1
Host: api.example.com
Origin: https://app.example.com

The server allows the response to be read:

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

Figure 2: Simple CORS Flow

Browser JS                 API server
    │                          │
    │ GET /products            │
    │ Origin: app.example.com  │
    ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€>│
    │                          │
    │ 200 OK                   │
    │ Allow-Origin: app        │
    │<─────────────────────────┤
    │                          │
    │ Browser exposes response │
    │ to JavaScript            │

Important nuance: simple does not mean safe. It only means the browser does not ask a preflight question first. The server must still protect state-changing behavior.

Preflight Requests

Why Preflight Exists

For requests that go beyond the safelisted shape, the browser first sends an OPTIONS request. This is called a preflight.

Common triggers:

•methods such as PUT, PATCH, or DELETE
•Content-Type: application/json
•custom headers such as Authorization or X-Request-ID
•non-safelisted request headers

Example frontend request:

fetch('https://api.example.com/orders/123', {
  method: 'PATCH',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer access-token'
  },
  body: JSON.stringify({ status: 'cancelled' })
})

Before the PATCH, the browser asks:

OPTIONS /orders/123 HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: PATCH
Access-Control-Request-Headers: authorization, content-type

The server approves:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PATCH, OPTIONS
Access-Control-Allow-Headers: authorization, content-type
Access-Control-Max-Age: 600
Vary: Origin

Then the browser sends the actual request.

Figure 3: Preflight Flow

Browser JS                 API server
    │                          │
    │ OPTIONS /orders/123      │
    │ Origin: app.example.com  │
    │ Request-Method: PATCH    │
    │ Request-Headers: auth... │
    ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€>│
    │                          │
    │ 204 No Content           │
    │ Allow-Origin: app        │
    │ Allow-Methods: PATCH     │
    │ Allow-Headers: auth...   │
    │<─────────────────────────┤
    │                          │
    │ PATCH /orders/123        │
    │ Origin: app.example.com  │
    ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€>│
    │                          │
    │ 200 OK + CORS headers    │
    │<─────────────────────────┤
    │                          │
    │ Browser exposes response │

Interview nuance: the actual request does not include Access-Control-Request-* headers. Those belong to the preflight request only.

Credentials and Cookies

Credentials Are Opt-In

Cross-origin requests do not automatically expose credentialed responses to JavaScript. Both sides need to opt in.

Client:

fetch('https://api.example.com/me', {
  credentials: 'include'
})

Server:

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

Critical rule:

Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

That combination is invalid for credentialed browser reads. With credentials, the allowed origin must be explicit.

Cookies Still Have Cookie Rules

CORS does not override cookie policy. Cross-site cookies may also need:

Set-Cookie: session=abc; HttpOnly; Secure; SameSite=None

or a same-site design where SameSite=Lax is enough.

CORS and CSRF Are Different

CORS decides whether JavaScript can read a cross-origin response.

CSRF is about unwanted state-changing requests sent with ambient credentials such as cookies.

A server can be vulnerable to CSRF even if CORS is strict, because a simple form-like request may still reach the server. CORS is not a CSRF defense by itself.

Headers You Must Understand

HeaderSent ByPurposeInterview Notes
OriginBrowser requestTells the server which origin initiated the requestPresent on CORS requests and many same-site state-changing requests
Access-Control-Allow-OriginServer responseSays which origin may read the responseUse exact origins for private APIs; * is only for public non-credentialed reads
Access-Control-Allow-MethodsServer preflight responseLists methods allowed for the actual requestNeeded for preflight approval
Access-Control-Allow-HeadersServer preflight responseLists request headers allowed for the actual requestMust include custom headers such as Authorization
Access-Control-Allow-CredentialsServer responseAllows credentialed browser reads when client opts inMust be true; cannot pair with wildcard origin
Access-Control-Expose-HeadersServer responseLets JS read non-safelisted response headersNeeded for headers like X-Request-ID or some pagination headers
Access-Control-Max-AgeServer preflight responseCaches preflight permissionBrowser caps may limit the effective duration
Vary: OriginServer responseTells caches the response varies by request originImportant when reflecting allowed origins dynamically
Access-Control-Expose-Headers: X-Total-Count, X-Request-ID

Then JavaScript can read it:

const response = await fetch(url)
console.log(response.headers.get('X-Total-Count'))

Server Implementation Patterns

Pattern 1: Public Read-Only API

Use this only when the response is intentionally public and no credentials are involved.

Access-Control-Allow-Origin: *

Good for public metadata, public assets, or open read APIs.

Pattern 2: Private Browser App API

Use an allowlist. Reflect only trusted origins.

const allowedOrigins = new Set([
  'https://app.example.com',
  'https://admin.example.com',
  'http://localhost:3000'
])

function cors(req, res, next) {
  const origin = req.headers.origin

  if (allowedOrigins.has(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin)
    res.setHeader('Vary', 'Origin')
  }

  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE, OPTIONS')
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Request-ID')
  res.setHeader('Access-Control-Allow-Credentials', 'true')
  res.setHeader('Access-Control-Max-Age', '600')

  if (req.method === 'OPTIONS') {
    return res.status(204).end()
  }

  next()
}

Pattern 3: Framework Middleware

Using package middleware is fine, but configure it intentionally:

app.use(cors({
  origin(origin, callback) {
    if (!origin) return callback(null, false)
    callback(null, allowedOrigins.has(origin))
  },
  credentials: true,
  methods: ['GET', 'POST', 'PATCH', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
  exposedHeaders: ['X-Request-ID']
}))

Avoid blindly reflecting any origin. Access-Control-Allow-Origin: req.headers.origin without validation is effectively public CORS with extra risk.

Debugging CORS

Read the Browser Console First

JavaScript usually receives a generic failure. The browser console explains the real CORS reason.

Debugging Checklist

1. Confirm the request origin.

Origin: https://app.example.com

2. Confirm the API response includes the expected allow origin.

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

3. If there is a preflight, inspect the OPTIONS response.

Access-Control-Allow-Methods: PATCH
Access-Control-Allow-Headers: authorization, content-type

4. If credentials are used, verify all three pieces:

fetch(url, { credentials: 'include' })
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://app.example.com

5. Check that the route handles OPTIONS without requiring application auth that only the actual request has.

6. Check redirects. Preflight plus redirects can behave differently across browsers and infrastructure.

7. Check CDN/proxy caching. If the server reflects origins, include Vary: Origin.

Do Not Fix CORS From the Frontend

These are usually not real fixes:

fetch(url, { mode: 'no-cors' })

no-cors gives you an opaque response that JavaScript cannot meaningfully read. It hides the error; it does not solve API access.

Browser extensions and dev proxies may help local debugging, but production CORS must be fixed at the server or gateway that serves the response.

CORS vs Security

What CORS Protects

CORS protects browser users by limiting what one origin's JavaScript can read from another origin.

It helps prevent a malicious page from silently reading sensitive cross-origin responses using the victim's browser context.

What CORS Does Not Protect

CORS does not:

•authenticate users
•authorize actions
•stop curl, Postman, mobile apps, or backend services
•validate input
•prevent CSRF by itself
•prevent XSS
•make public data private

Common Bad Configurations

Reflecting Every Origin

res.setHeader('Access-Control-Allow-Origin', req.headers.origin)

This is unsafe unless origin was checked against a trusted allowlist.

Combining Credentials With Broad Access

Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://any-origin-from-request.example

If the origin is not validated, any site can read credentialed responses.

Treating CORS Errors as Auth Errors

A CORS failure means the browser refused to expose the response. The request may have failed auth, failed preflight, hit a proxy, missed a header, or been blocked after a redirect. Inspect the network panel.

Interview Answer

QuestionGood Answer
Why does Postman work but browser fails?Postman is not enforcing browser CORS. CORS is a browser security model.
Why did adding Authorization trigger OPTIONS?Authorization is not a safelisted request header, so the browser preflights.
Can I use * in production?Yes for truly public non-credentialed responses; not for credentialed private APIs.
Why is my custom response header missing?The server may need Access-Control-Expose-Headers.
Is CORS a CSRF defense?No. CORS controls JS read access; CSRF is about unwanted credentialed state changes.

Key Takeaways

1CORS is enforced by browsers, not by servers or non-browser clients.
2CORS controls whether JavaScript can read a cross-origin response.
3Preflight is an OPTIONS permission check for non-safelisted methods or headers.
4Credentialed CORS requires explicit client and server opt-in.
5CORS is not authentication, authorization, CSRF protection, or input validation.
6The safest production pattern is an exact origin allowlist plus Vary: Origin when origins are reflected.