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.
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:
- Browser tags the request: adds an
Originheader with the requesting page's origin - Server replies with permission: includes
Access-Control-Allow-Origin: https://your-frontend.com - Match: browser lets your JS read the response
- 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.
| Context | Cookies Sent? | CORS Applied? | Why? |
|---|---|---|---|
| Browser, same-origin fetch | Yes (if in cookie path) | No (same-origin, always allowed) | No cross-origin risk |
| Browser, cross-origin fetch | Yes (if credentials: 'include') | Yes (must whitelist) | Protect user from attack |
| curl, any URL | Only if you pass --cookie | No | No browser, no user context |
| Server-to-server HTTP request | Only if you set them | No | No 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.