Cross- Origin Resource Sharing.

The most misunderstood ten lines of the web platform. You've seen the red console error. You've pasted the Stack Overflow fix. This page makes you actually understand it — by letting you poke a simulated browser and server until it clicks.

§ 01 — First principles

What even is an origin?

An origin is the triple scheme + host + port. Nothing more, nothing less. The path doesn't matter. The query string doesn't matter. But a different subdomain, a different port, or http vs https — each one makes a completely different origin in the browser's eyes.

Type two URLs below (or hit a preset) and see how the browser compares them:

URL A the page you're on

URL B the thing you fetch()

Gotcha https://cors.in and https://cors.in:443 are the same origin — 443 is the default port for https, so browsers normalize it away. But https://cors.in:8443 is a stranger.

§ 02 — The rule CORS relaxes

The same-origin policy was already there.

Since the 1990s, browsers have enforced one big rule: JavaScript on one origin may not read responses from another origin. Why? Because your browser carries your cookies everywhere. Without this rule, any page you visit could quietly fetch() your webmail, your bank dashboard, your company intranet — with your session cookies attached — and read the results.

Here's the reframe that fixes most confusion: CORS is not a wall. CORS is a door in a wall that already existed. It's the mechanism a server uses to say "actually, browser, it's fine — let that other origin read my responses." Every CORS header you add makes the web less locked-down, not more.

Cross-origin was always allowed

  • Loading images: <img src="other.site/pic.png">
  • Loading scripts & styles: <script src>, <link>
  • Submitting forms: <form action="other.site">
  • Embedding frames: <iframe> (it renders; you can't reach in)
  • Plain links, redirects, navigations

Cross-origin reads are blocked

  • fetch() / XMLHttpRequestreading the response
  • Reading pixels of a cross-origin image via canvas
  • Reaching into a cross-origin iframe's DOM
  • Reading another origin's cookies or storage
The insight most tutorials skip For "simple" requests, the browser still sends your request to the server. The server receives it, executes it, and responds. CORS only stops your JavaScript from reading the answer. If your endpoint has side effects — a POST /transfer-money — a CORS error in the console does not mean the transfer didn't happen.

§ 03 — The playground

Break it yourself. On purpose.

You're JavaScript running on https://app.example.com. Configure the request on the left and the server on the right, then run it and watch what actually crosses the wire — including the invisible preflight OPTIONS request browsers send before "non-simple" requests.

🧑‍💻 Browser — https://app.example.com

🖥 Server — api.example.com

This server answers preflights with
Allow-Methods: GET, POST, PUT, DELETE
Allow-Headers: Content-Type, X-API-Key

§ 04 — Case study

The day Zoom decided CORS was too annoying.

In 2019, Zoom's Mac client ran a hidden web server on http://localhost:19421 so that zoom.us could launch meetings without a click. To talk to it from the browser, Zoom needed CORS headers on that local server.

CORS felt like it was "in the way" — so instead of sending the headers, they bypassed it entirely: the page loaded images from the localhost server (image loads don't need CORS, remember §02), and the local server encoded its status into the pixel dimensions of the image. The page read img.width to learn the result.

Clever. Except CORS was never the thing in the way — it was the thing protecting users. Since the trick worked from zoom.us, it worked from every website on the internet. Any page you visited could command your Zoom client: join a meeting, camera on, no confirmation. It got worse — the same channel could trigger reinstalling Zoom after you'd deleted it.

And no, localhost is not exempt: browsers apply the same-origin policy to localhost like anywhere else. CORS wasn't broken here. CORS was the correct tool, deliberately routed around.

literally-any-website.com<img src="http://localhost:19421/launch?…">

▼  image request — no CORS check applies
localhost:19421Zoom's hidden web server, running as you

▼  replies with an image; its width × height = status code
Zoom.app launchesmeeting joined, camera on 📹
The entire correct fix Access-Control-Allow-Origin: https://zoom.us
One header. Only JavaScript running on zoom.us can read the local server's responses. Every other origin stays locked out.

§ 05 — Pop quiz

Six myths that keep shipping to production.

True or false? Answer honestly — these come from real Stack Overflow answers with thousands of upvotes.

Score: 0 / 0 answered

§ 06 — Take-home

The cheatsheet.

Do

  • Keep an explicit allowlist of origins; echo the matching one back in Access-Control-Allow-Origin.
  • Send Vary: Origin whenever the header depends on the request — or caches will serve one origin's permission to another.
  • With cookies: exact origin + Access-Control-Allow-Credentials: true. The browser refuses * here — by design.
  • Answer OPTIONS preflights fast, and cache them with Access-Control-Max-Age.
  • Remember curl, servers, and attackers' scripts ignore CORS entirely — authenticate and CSRF-protect your API regardless.

Don't

  • Slap Access-Control-Allow-Origin: * on an authenticated API because a blog post said so.
  • Reflect any incoming Origin header back with credentials enabled — that's * with extra steps, and worse.
  • Route production traffic through public "CORS proxies" — you're handing every request and its tokens to a stranger.
  • Develop with --disable-web-security. It "fixes" your app and disables the protection for every site in that browser.
  • Sidestep CORS with tricks (image pixels, JSONP, form posts). Ask Zoom how that ends. §04 ↑

Keep reading: this page was inspired by Chris Foster's essay “Developers don't understand CORS” — the full Zoom write-up is worth your time. Then bookmark MDN's CORS reference for the day preflight caching bites you.