CORS from the server side — what it actually is and isn't protecting

CORS is a browser-enforced restriction on the CLIENT side, not a server-side security feature — the server's job is only to explicitly grant permission via a response header, and misunderstanding that direction of control causes most real CORS confusion.

Intermediate

4 min read

The same-origin policy: the browser default CORS headers actually relax

A page loaded from https://app.example.com trying to fetch()
https://api.example.com/data — DIFFERENT origin (different subdomain) —
is BLOCKED by the browser's own same-origin policy by default, regardless
of what the server itself would have allowed

Browsers enforce a default rule called the same-origin policy: JavaScript running on one origin (scheme + domain + port, all three) can't freely read responses from a different origin via fetch/XHR, purely as a browser-side security measure protecting a user from a malicious page silently reading data from another site the user happens to be logged into. CORS (Cross-Origin Resource Sharing) is the mechanism that relaxes this default restriction, when the server explicitly opts in — it's not an additional restriction the server has to defend against; it's a permission the server can choose to grant.

Access-Control-Allow-Origin: the server explicitly granting permission

app.use((req, res, next) => {
  res.setHeader("Access-Control-Allow-Origin", "https://app.example.com"); // explicit permission for THIS origin
  next();
});
 
// Or with the cors package:
const cors = require("cors");
app.use(cors({ origin: "https://app.example.com" }));

The server sets this response header explicitly, naming which origin(s) are allowed to read the response — the browser checks this header on the client side and only then allows the requesting page's JavaScript to actually access the response body; without it (or with a mismatched origin), the browser blocks access to the response, even though the server already fully processed the request and sent a complete response back over the network. This is the detail that trips people up: a CORS failure doesn't mean the request never reached the server — it usually did, and the server usually responded — it means the browser refused to let the calling page's JavaScript read that response afterward.

What CORS does NOT protect: it's not authentication or authorization

// CORS says WHICH BROWSER-BASED ORIGINS may read a response — it says
// NOTHING about whether the request itself required a valid credential.
// A tool like curl, Postman, or a server-to-server request completely
// IGNORES CORS entirely — CORS is a BROWSER-enforced restriction only.

CORS restricts which web-page origins a browser will let JavaScript read a response from — it does nothing to restrict requests made outside a browser context (a server calling another server, a command-line tool, a mobile app) since those don't have a browser's same-origin policy to enforce in the first place. This is a genuinely important, common misunderstanding: setting a permissive CORS policy doesn't make an API insecure by itself (since CORS was never providing security against non-browser callers), and a restrictive CORS policy doesn't make an API secure by itself either — real authentication and authorization (covered in this platform's Auth domain) are the actual security mechanism; CORS is purely about which browser pages get to read a response.

Preflight requests: the browser checking permission before sending the real one

For "non-simple" requests (custom headers, methods like PUT/DELETE/PATCH,
or a Content-Type other than a few basic ones), the browser automatically
sends an OPTIONS request FIRST — asking "would you allow this?" — before
sending the actual request at all:

OPTIONS /api/orders  (the preflight — asks permission)
  ← Access-Control-Allow-Methods: GET, POST, PUT, DELETE
  ← Access-Control-Allow-Headers: Content-Type, Authorization

PUT /api/orders/42   (the REAL request, only sent if the preflight allowed it)

For requests beyond a small "simple request" allowlist, the browser automatically sends a preflight OPTIONS request first, asking the server which methods and headers it permits from the calling origin — the actual request only gets sent afterward, and only if the preflight response indicates it's allowed. A server needs to explicitly handle OPTIONS requests (usually automatically, via a library like the cors package) for this to work — an API that only implements GET/POST/PUT handlers but never responds correctly to OPTIONS will silently fail every preflighted cross-origin request, even though the "real" request method is fully implemented.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. Is CORS a server-side security restriction the server has to defend against?

2. When a CORS error occurs, did the request actually reach the server?

3. Does a permissive CORS policy make an API insecure to callers outside a browser, like curl or a server-to-server request?