Code Safari

Chapter 94·Beginner·10 min read

The Same-Origin Policy & CORS, Explained: Why the Browser Blocks Your Fetch

What is CORS actually for? A plain-English explanation of the same-origin policy — the browser's core security model — why that 'blocked by CORS policy' error exists, how preflights work, and the misconfigurations that turn CORS from shield into hole.

July 23, 2026

Every web developer meets it eventually, red in the console: "…has been blocked by CORS policy." Most fight it, find a header that makes it stop, and move on — never learning that they just negotiated with the single most important security mechanism in the browser.

This chapter explains what's actually going on: the rule (the same-origin policy), the relaxation mechanism (CORS), and the misconfigurations that quietly disable a protection you didn't know you were relying on. It's the foundation the next two chapters build on.

The problem the browser must solve

Right now you might have open: your email, your bank, and some site you've never visited before running JavaScript you've never read. They're all executing inside the same program — your browser — and, thanks to cookies, your bank tab is logged in.

Chapter 1's question applies: what stops that unknown site's JavaScript from simply making a request to yourbank.com/accounts — which the browser would helpfully send with your bank cookies — and reading the response?

One rule stops it. The same-origin policy (SOP): scripts running on one origin may not read responses from a different origin. It is the load-bearing wall of web security. Without it, visiting any webpage would hand that page all the data every other logged-in site would show you.

What exactly is an origin?

An origin is the triple scheme + host + port, matched exactly:

URL AURL BSame origin?
https://example.com/page1https://example.com/api✅ Paths don't matter
https://example.comhttp://example.com❌ Different scheme
https://example.comhttps://api.example.com❌ Different host — subdomains count
https://example.comhttps://example.com:8443❌ Different port

That third row is the one that bites teams daily: the moment you serve your frontend from app.example.com and your API from api.example.com, you are cross-origin to yourself, and the wall applies to you too. Which is why CORS exists.

CORS: the server's permission slip

Cross-Origin Resource Sharing is not the blocker — it's the unlock. It's a protocol by which a server can tell browsers: "responses of mine may be read by scripts from these specific other origins."

The mechanics are refreshingly simple. When JavaScript on https://app.example.com fetches https://api.example.com/data:

  1. The browser attaches a header to the request: Origin: https://app.example.com.
  2. The server responds, optionally including: Access-Control-Allow-Origin: https://app.example.com.
  3. The browser checks: does the response grant permission to the requesting origin? If yes, the JavaScript gets to read it. If no — the famous error.

Note carefully where enforcement happens: in the browser, after the response arrives. Which sets up the most misunderstood fact in this whole topic.

CORS protects users, not your API

The request still reached your server. The server still executed it and produced a response. The browser then withheld that response from the page's JavaScript. So:

  • CORS/SOP protect users — they stop evil.com from using a victim's browser (and the victim's cookies) as a logged-in proxy to read other sites' data.
  • They do not protect your API — an attacker with curl, Postman, or a script isn't a browser and doesn't play by browser rules. They can send any request with any Origin header, or none.

If you've ever heard "we don't need auth on that endpoint, CORS blocks other sites" — that's the misunderstanding in the wild. Your API's protection is authentication and authorization, always. CORS is about which websites may act through your users' browsers.

Preflights: the OPTIONS request you didn't send

Watch your network tab and you'll see the browser sometimes sends an OPTIONS request you never wrote, before your actual request. That's a preflight: for requests that couldn't have been produced by an old-fashioned HTML form — say, a PUT, a DELETE, or anything with a Content-Type: application/json header — the browser first asks permission before sending the real request at all.

JS calls PUT api.example.com
Browser sends OPTIONS preflight
Server: allowed origins/methods/headers
Real PUT sent only if approved
A preflighted request: permission first, real request second.

Why the distinction? Backwards compatibility with the pre-CORS web. Simple GETs and form-style POSTs have always been sendable cross-origin (that's just how links and forms work — a fact that becomes the entire CSRF story next chapter), so blocking them now would break the internet; for those, the browser just withholds the response. But anything beyond what forms could do gets the stricter treatment: no permission, no request. The Access-Control-Max-Age header lets servers cache preflight approvals so you don't pay the round-trip every time.

How CORS configuration goes wrong

CORS misconfiguration is its own vulnerability class, and two patterns account for most of it:

1. The reflected-origin footgun. A developer, tired of maintaining a list, configures the server to echo back whatever Origin arrives — and, because the app uses cookies, also sets Access-Control-Allow-Credentials: true. Read that combination against the opening scenario: any website on the internet may now make credentialed requests to your API through a visitor's browser and read the responses. You have surgically re-created the attack the same-origin policy exists to prevent. (The spec authors saw this coming: browsers refuse the literal * wildcard when credentials are allowed — reflecting origins is how people bypass their own safety rail.)

2. Trusting null or unvalidated subdomains. Allowing the null origin (sent by sandboxed iframes and some redirects) or pattern-matching origins sloppily (endsWith("example.com") matches evilexample.com) reopens the same door more quietly.

The correct shape is boring: an explicit allowlist of exact origins, credentials only if genuinely needed, reviewed like any other security config.

Recap

  • The same-origin policy is the browser's core security wall: scripts from one origin can't read another origin's responses — it's why open tabs can't loot each other.
  • An origin = scheme + host + port, exactly — your own subdomains are cross-origin to you.
  • CORS is the server-granted exception, not the blocker; the console error is the wall working as designed, and the fix always lives server-side.
  • It protects users, not APIs — attackers with curl skip the browser entirely; your API still needs real auth.
  • Preflights ask permission before non-form-shaped requests; form-shaped ones send anyway (the seam CSRF lives in).
  • The classic misconfiguration — reflected origin + credentials — rebuilds the exact attack SOP prevents; allowlist exact origins instead.

The wall has one gap we just noted: requests that look like forms still go through, cookies attached. Next chapter, we watch attackers walk straight through it — and then we seal it. But first, the other injection into the browser: XSS, explained.

The Same-Origin Policy & CORS, Explained: Why the Browser Blocks Your Fetch | Code Safari