DEV Community

Cover image for CORS — the attack it stops, and the one-line fix
Vahid Aghajani
Vahid Aghajani

Posted on • Originally published at software-engineer-blog.com

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

📺 Prefer to watch? 90-second YouTube Short · 💬 Telegram

Originally published on software-engineer-blog.com.

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))
Enter fullscreen mode Exit fullscreen mode

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 */ })
Enter fullscreen mode Exit fullscreen mode

Request headers:

GET /data HTTP/1.1
Origin: https://app.example.com
Enter fullscreen mode Exit fullscreen mode

Response headers:

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

{"balance": 1000}
Enter fullscreen mode Exit fullscreen mode

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' }
})
Enter fullscreen mode Exit fullscreen mode

Browser sends:

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

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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' }));
Enter fullscreen mode Exit fullscreen mode

FastAPI:

from fastapi.middleware.cors import CORSMiddleware

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

nginx:

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

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.

Top comments (0)