CORS: What is it protecting? | Engineering Notes before<br>the browser paints. Runs synchronously, no defer/async.<br>--><br>Skip to content<br>Go back<br>CORS: What is it protecting?<br>27 Jun, 2026
A friend once asked me what CORS was. I explained it the way most of us first learn it: you tell your server which domains are allowed to send requests. On a dev machine you allow localhost so your local frontend can talk to the backend. In production you allow only your real frontend domain.
It felt simple, and it mostly worked in my head.
A few weeks later I gave the same explanation in an interview. The interviewer nodded and then asked one short question: is CORS a browser security measure or a server security measure?
I went with server. We are protecting our backend from unknown domains, so it has to be the server doing the guarding. Right?
Wrong. And the gap between what I said and what actually happens is the part worth writing down.
Table of contents
Open Table of contents
Browser or server security?
How it is implemented?
Preflight, request and response
So what is CORS actually protecting?
CORS is not a CSRF defense
Wrapping up
Browser or server security?
CORS lives in the browser. The server is not the one enforcing it.
Here is the test that makes it obvious. Take any endpoint that returns a CORS error in the browser, then hit the same endpoint with curl or Postman. It returns the data without complaint. No Origin check, no block, nothing. The server happily answered. The browser was the only thing that ever cared about the origin.
So CORS is a set of rules a browser follows about which cross-origin responses it will let your JavaScript read. It only comes into play when your frontend and backend sit on different origins. Same origin, no CORS at all. A Next.js app that serves its frontend and API routes from one domain never trips over it, because the browser sees same-origin requests and stays out of the way.
How it is implemented?
The browser attaches an Origin header to cross-origin requests, set to the page’s own origin. When the response comes back, the browser looks for an Access-Control-Allow-Origin header. If that header matches the origin, your JavaScript gets to read the response. If it does not match, the request still reached the server and the server still ran it, but the browser throws away the response before your code can touch it.
The server already did the work. The block happens on the way back, inside the browser.
Some requests get an extra step. If the request uses a method other than GET, POST, or HEAD, or carries custom headers, or sends a Content-Type like application/json, the browser does not send it straight away. It first sends a preflight: an OPTIONS request that asks the server, in advance, whether the real request is allowed. Most requests in a real app fall into this category, so preflights are the common case.
Preflight, request and response
Here is a preflight the browser sends before a PUT that carries an auth token:
OPTIONS /users HTTP/1.1<br>Origin: https://frontend.com<br>Access-Control-Request-Method: PUT<br>Access-Control-Request-Headers: Authorization, Content-Type<br>Read it as the browser checking ahead: I am about to send a PUT from https://frontend.com, and it will carry Authorization and Content-Type headers. Are all of those allowed?
The server answers:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://frontend.com<br>Access-Control-Allow-Methods: GET, POST, PUT<br>Access-Control-Allow-Headers: Authorization, Content-Type<br>Access-Control-Allow-Credentials: true<br>Access-Control-Max-Age: 3600<br>Each header answers part of the question:
Access-Control-Allow-Origin says which origin may read the response. Here, only https://frontend.com.
Access-Control-Allow-Methods lists the methods the server will accept cross-origin.
Access-Control-Allow-Headers lists the request headers it will accept. If the real request sends a header not on this list, the browser blocks it.
Access-Control-Allow-Credentials: true allows the browser to expose the response to JavaScript when a cross-origin request includes browser-managed credentials (such as cookies). It cannot be used together with Access-Control-Allow-Origin: *. The server must specify the exact allowed origin.
Access-Control-Max-Age is how long the browser may cache this preflight result. Set to 3600, the browser skips the OPTIONS round trip for the next hour. Leave it off and you pay a preflight before nearly every request, which is a real latency tax on a chatty frontend.
All of this hinges on one match: the Origin the browser sent and the Access-Control-Allow-Origin it got back have to agree. Everything else is detail layered on top of that one comparison.
There are several other headers. You can check them out in the MDN docs.
So what is CORS actually protecting?
If CORS only runs in the browser, why not copy the request into Postman, change the Origin, and replay it? You can. But from Postman you send...