← Computer Networks
Sessions, Auth & Browser Security

CORS

Watch: "CORS in 100 Seconds" — Fireship

CORS is a browser-enforced security mechanism, not a server-side one — and that single distinction is the correction interviewers make most often when candidates talk about it. Getting it right shows you understand where trust boundaries actually live on the web. If your frontend is https://app.example.com and your API is https://api.example.com, the browser treats those as different origins — different subdomain, so different origin — and locks down what JavaScript on one can read from the other by default.

What Is CORS?

CORS (Cross-Origin Resource Sharing) is the set of rules a browser follows to decide whether a webpage from one address is allowed to fetch data from a different address. Without it, any website you happen to visit could quietly ask your browser to pull data from, say, your bank's site using your logged-in session — so browsers block that by default, and CORS is the mechanism a server uses to explicitly say "yes, this other website is allowed to talk to me."

Where This Is Used

A Day-to-Day Example

Picture an office building where each company on a different floor has its own reception desk. If someone from Floor 2 shows up asking Floor 5's receptionist for a file, the receptionist checks a list of who's allowed in before handing anything over — even if the visitor seems harmless. CORS is that receptionist's checklist: the browser is the visitor, the website you're loading data from is the floor being asked, and the list of allowed visitors is set by whoever runs that floor, not by the visitor themselves.

Deep Dive: Simple Requests vs Preflighted Requests

Not every cross-origin request triggers a preflight — the browser only preflights requests it considers potentially unsafe by the older same-origin-form baseline. A "simple" request goes straight to the server:

GET /public-data 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
Content-Type: application/json

{"items":[...]}

The browser sends this immediately, gets the response, and only then checks whether Access-Control-Allow-Origin permits https://app.example.com to actually read it in JavaScript. If it doesn't match, the response is blocked from the page's script — but note the request still reached the server and the server still did whatever work it does. A request with a custom header, a non-whitelisted method, or a body type like application/json is "preflighted":

CORS preflight before a DELETEapp.example.com (browser)api.example.comOPTIONS /users/123 (preflight)may I DELETE with auth?204 + Allow-Origin / Methods / Headerscached 24 h (Max-Age)DELETE /users/123only now the real callresponse + Allow-Origin
For anything beyond a "simple" request (e.g. DELETE, or an Authorization header), the browser first asks the API with OPTIONS whether this origin, method and headers are allowed. Only if the answer allows it does the real request go out. The raw messages are below.
browserAPI
OPTIONS /users/123 HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: authorization

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400

--- only now does the browser send the real request ---

DELETE /users/123 HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Access-Control-Max-Age lets the browser cache the preflight result so it doesn't re-ask OPTIONS before every single matching request — a meaningful latency saver for an API that gets hit frequently from the same origin.

Deep Dive: Credentials and the Wildcard Trap

// Client must explicitly opt in to sending cookies cross-origin:
fetch("https://api.example.com/me", { credentials: "include" });

// Server MUST echo back the specific origin, never a wildcard, when
// credentials are involved:
Access-Control-Allow-Origin: https://app.example.com   ✓ works with credentials
Access-Control-Allow-Origin: *                          ✗ browser rejects if
Access-Control-Allow-Credentials: true                     credentials were sent

This is a deliberate spec restriction, not a bug: if a wildcard origin were allowed alongside credentials, any site on the internet could make a credentialed request to your API using the victim's browser session and read the response — exactly the CSRF-adjacent risk the spec is designed to prevent. Servers that need to support multiple specific origins with credentials typically reflect the request's Origin header back dynamically (after checking it against an allowlist) rather than hardcoding one.

Key Pointers

How It Works

  1. A page on origin A makes a fetch/XHR request to origin B.
  2. The browser checks whether the request is "simple"; if not, it sends an OPTIONS preflight request first, carrying Access-Control-Request-Method/Headers.
  3. Server B responds to the preflight with its Access-Control-Allow-* headers describing what it permits.
  4. If the preflight response permits the method, headers, and origin, the browser sends the real request.
  5. Server B processes the real request and responds; the browser only exposes that response to the page's JavaScript if Access-Control-Allow-Origin matches origin A.

Try It Yourself

# Simulate a cross-origin fetch's preflight manually
curl -X OPTIONS https://api.example.com/users/123 \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: DELETE" \
  -H "Access-Control-Request-Headers: authorization" \
  -v

# See exactly which Access-Control-* headers a real endpoint returns
curl -H "Origin: https://app.example.com" -I https://api.example.com/public-data

# In Chrome DevTools: Network tab → make the cross-origin request from your
# app → a blocked response shows a red "CORS error" in the Console tab with
# the specific header that was missing or mismatched, and you'll see BOTH
# the OPTIONS preflight and the real request as separate entries if preflighted

# Confirm curl isn't affected by CORS at all — it will happily return the
# body even with no Access-Control-Allow-Origin header present
curl https://api.example.com/admin-only-from-browser

Interview Q&A

Q: If a CORS error shows up in the browser console, did the request actually reach the server?

A: For a "simple" request, yes — it was sent and processed by the server; the browser only blocks the page's JavaScript from reading the response afterward. So if that request was a POST that wrote data, the write still happened even though the frontend never sees a successful response — this is exactly why mutating endpoints can't rely on CORS as their only protection and still need CSRF defenses. For a preflighted request, if the preflight itself is rejected, the real request is never sent at all.

Q: Why doesn't curl or Postman run into CORS errors the way a browser does?

A: Because CORS is a restriction the browser imposes on scripts running inside a web page, as an extension of the same-origin policy — it exists to protect users browsing the web, not to protect servers. curl and Postman aren't a page's embedded JavaScript acting on a user's behalf; they're just HTTP clients with no same-origin concept at all, so there's nothing for CORS to enforce.

Q: You're getting a CORS error and your fix is to set Access-Control-Allow-Origin: * on the API. What's wrong with that?

A: It works for unauthenticated, public data, but it's a common security smell for anything sensitive: a wildcard can't be combined with credentialed requests at all (the browser will reject that combination outright), so it either silently breaks cookie-based auth or, if the API doesn't need credentials, it means literally any website on the internet can read that API's responses from a user's browser. The correct fix is allowlisting specific trusted origins, not opening it to everyone by default.

Q: What triggers a preflight versus a simple request?

A: Method and headers/content-type. GET, HEAD, and POST with only a small set of "CORS-safelisted" headers and a content type of application/x-www-form-urlencoded, multipart/form-data, or text/plain count as simple. Anything else — a PUT/DELETE/PATCH, a custom header like Authorization or X-Request-Id, or a JSON body with Content-Type: application/json — triggers a preflight OPTIONS request first. This is why almost every real JSON API triggers a preflight on every non-GET request.

System Design Tie-in

CORS configuration comes up any time your architecture has a frontend and backend on different origins — which is the norm, not the exception, for anything split into a SPA plus an API, or a public API meant to be called directly from other companies' frontends. The expected design answer is an explicit origin allowlist (not a wildcard) for any endpoint that accepts credentials, with preflight caching (Access-Control-Max-Age) tuned to avoid a redundant OPTIONS round trip on every request from a high-traffic frontend. It also matters for platform/API-product design: if you're building a public API meant to be called directly from third-party browser-based apps (like Stripe.js), you deliberately keep it permissive on CORS for public, non-sensitive endpoints, while requiring server-to-server calls (with a secret key, never exposed to a browser) for anything sensitive — CORS is a browser-only door, so anything that shouldn't be reachable from arbitrary browser JavaScript shouldn't rely on CORS restrictions as its actual security boundary; it needs real authentication.

Trade-offs

A common shortcut is "fixing" a CORS error by setting Access-Control-Allow-Origin: * everywhere — this breaks credentialed requests and is a real security smell for any API carrying sensitive data; allowlisting specific origins is the correct fix. The other classic gotcha: a blocked CORS response still reached the server and may have caused a side effect (a POST that wrote data) even though the browser blocked the page from reading the response — CORS protects response confidentiality, not request execution, so mutating endpoints still need CSRF protection on top of it.