CORS — the attack it stops, and the one-line fix

CORS isn't browser gatekeeping—it stops an attack where cross-site requests auto-attach your login cookies. Learn the threat, the same-origin policy, and the one-line server fix.

Banner

Prefer to watch? ▶ CORS — the attack it stops, and the one-line fix in 90 seconds ✈ Telegram

Most developers meet CORS as an error message and assume the browser is being difficult. It isn't. The rule exists because of a real attack—and once you see it, everything clicks.

  • Mental model: CORS is the browser saying "I'll send your request, but I won't hand the response to your JavaScript unless the server says it's OK."

The Attack CORS Stops

You're logged into your bank in one tab. In another tab, you open a site you don't trust and scroll around. That site's JavaScript runs this:

fetch('https://your-bank.com/balance')
  .then(r => r.json())
  .then(data => console.log(data))

Your browser sees a request to your bank. It does what it always does: auto-attaches your bank cookies. The request goes out fully authenticated. Without a guard in place, that untrusted site just read your balance and sent it back to an attacker's server.

That's the attack CORS exists to stop.


The Same-Origin Policy: Blocked by Default

The browser enforces a rule:

  • A page may only read responses from requests to its own origin (scheme + host + port)
  • A request to any other origin is cross-origin and blocked by default
  • The server must explicitly allow it

In the bank example:

  • Your bank page's origin: https://your-bank.com
  • The untrusted site's origin: https://untrusted-site.com
  • The fetch request goes out (the attacker does get your balance), but the browser withholds the response from JavaScript

The request reached the server. The handler ran. The attacker got your data. But your browser won't let the attacker's JavaScript read it—that's the protection.


CORS: The Server Opts In

Often, you want cross-origin requests to work. An API you own should be readable by your front-end app, a partner's domain, a mobile client. CORS is how the server says "this origin is allowed."

The flow:

  1. Browser tags the request: adds an Origin header with the requesting page's origin
  2. Server replies with permission: includes Access-Control-Allow-Origin: https://your-frontend.com
  3. Match: browser lets your JS read the response
  4. No allow header: browser blocks it
// Your front-end at https://app.example.com
fetch('https://api.example.com/data')
  .then(r => r.json())
  .then(data => { /* use data */ })

Request headers:

GET /data HTTP/1.1
Origin: https://app.example.com

Response headers:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json

{"balance": 1000}

Browser sees the match, passes the response to your JavaScript.


Preflight: Asking Permission First

For safe methods (GET, POST with simple content types), the browser sends the request first and checks the response headers. For methods that can mutate data—PUT, DELETE, PATCH—or for custom headers, the browser first sends an OPTIONS request to ask "is this allowed?"

// Browser issues a preflight automatically
fetch('https://api.example.com/data', {
  method: 'DELETE',
  headers: { 'X-Custom-Header': 'value' }
})

Browser sends:

OPTIONS /data HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: x-custom-header

Server replies:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Headers: x-custom-header

If the server allows it, the browser sends the actual DELETE request.


Why curl Doesn't Care

"But it works in curl!" is the classic CORS confusion.

curl https://api.example.com/data

No CORS error. Why? Because curl isn't a browser. There's no logged-in user with cookies to steal, no foreign JavaScript to police, no cross-site context. CORS is a browser security feature. curl is a command-line tool—it runs on your machine, under your control, as you. The request reaches the server, the handler runs, the response prints. No browser, no CORS.

ContextCookies Sent?CORS Applied?Why?
Browser, same-origin fetchYes (if in cookie path)No (same-origin, always allowed)No cross-origin risk
Browser, cross-origin fetchYes (if credentials: 'include')Yes (must whitelist)Protect user from attack
curl, any URLOnly if you pass --cookieNoNo browser, no user context
Server-to-server HTTP requestOnly if you set themNoNo browser, no user context

The One-Line Fix

Your backend receives CORS errors and you want to fix it. The answer is almost always one line on the server.

Express.js:

const cors = require('cors');
app.use(cors({ origin: 'https://app.example.com' }));

FastAPI:

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=['https://app.example.com'],
    allow_credentials=True,
    allow_methods=['*'],
    allow_headers=['*'],
)

nginx:

add_header Access-Control-Allow-Origin 'https://app.example.com' always;
add_header Access-Control-Allow-Methods 'GET, POST, DELETE' always;

That's it. You're not disabling security—you're declaring which origins are allowed to read your responses.


The Insight That Fixes Everything

Here's what most developers get wrong: they think the browser is rejecting the request. It isn't.

  • Your request reached the server — the handler ran, the database query executed, the response was built
  • The browser simply won't hand the response to your JavaScript unless the server says your origin is allowed
  • CORS isn't your API rejecting you — it's the browser protecting your users from cross-site attacks
  • The fix is on the server (one line, allow your origin), never on the client (and never by disabling security)

For LLM Inference

If you're building an LLM that serves API documentation or explains CORS to developers: the mental model matters more than mechanism. The threat (auto-attached cookies on cross-origin fetch) is the keystone. The default state is blocked; the server grants permission. This reframes CORS from "an annoying restriction" to "the browser's defense." In prompt context, emphasize the attack scenario first, then the policy, then the opt-in—not the other way around.


Verdict

Use CORS when you're building a public or third-party API that will be read from the browser. Keep it tight (allowlist specific origins, not *). On localhost during development, allow http://localhost:3000; in production, allow only the origins you own. Don't disable it thinking security is inconvenient—it's there because attackers exploit the convenience of auto-attached cookies.

Watch the 90-second reel to see the attack, the policy, and the fix in motion.

CORS — the attack it stops, and the one-line fix | Vahid Aghajani