CORS Explained Simply: Why You Shouldn't Just Disable It
IntroductionDev EnvironmentClientTransportServerAppDataMore

CORS Explained Simply: Why You Shouldn't Just Disable It

March 21, 2025

You're logged into your banking app. In another tab, you're browsing what looks like a news site — but that site has a hidden script that fires the moment the page loads. It sends a request to api.yourbank.com/transfer using your already-authenticated session. Without CORS, the bank's API would accept the request and send back your account details, or worse — process the transfer.

CORS is what stops that from happening. And "just disable it" is one of the most dangerous things you can do to your app.

The Threat — Cross-Origin Requests Without Guard Rails

Browsers let JavaScript make HTTP requests to any URL. That's by design — it's how your frontend talks to your API. The problem is that it also means code running on evil.com can try to talk to api.yourbank.com using your browser, where your session cookies are already stored.

Without the Same-Origin Policy and CORS, any website a user visits could silently:

  • Read private data from APIs where the user is authenticated
  • Submit forms or trigger actions on behalf of the user
  • Extract session tokens or personal information accessible through an authenticated API

The attack doesn't require stealing credentials. It hijacks the user's existing authenticated session — from a completely different domain.

Consequences — What Disabling CORS Actually Means

Setting Access-Control-Allow-Origin: * on every endpoint — or stripping CORS headers at a proxy level — removes a browser-level security guarantee. The practical outcomes:

  • Session hijacking via CSRF — authenticated requests can be triggered from attacker-controlled pages. Classic example: bank.com's /transfer endpoint called silently from a malicious site.
  • Data exfiltration — internal APIs, admin dashboards, or any endpoint the user's browser has access to can be read by cross-origin scripts.
  • Unauthorized API consumption — third-party sites can build scrapers or services that call your API on behalf of your own users, bypassing your terms of service and rate limits, billing the cost to your infrastructure.

The Defense: CORS and the Same-Origin Policy

CORS (Cross-Origin Resource Sharing) is how browsers control cross-origin access in a controlled, permission-based way.

At the foundation is the Same-Origin Policy: by default, JavaScript running on https://myapp.com can only make requests back to https://myapp.com. Anything involving a different protocol, domain, or port is a different origin and is blocked.

An "origin" is the combination of:

  • Protocol (https vs http)
  • Domain (myapp.com vs api.myapp.com)
  • Port (443 vs 8080)

So these are all different origins, even if they feel related:

  • https://myapp.com vs. http://myapp.com (different protocol)
  • https://myapp.com vs. https://api.myapp.com (different subdomain)
  • https://myapp.com vs. https://myapp.com:8080 (different port)

CORS is a controlled relaxation of that policy. The server declares which origins it trusts, and the browser enforces that declaration. The key headers:

  • Access-Control-Allow-Origin — which origins can access this resource
  • Access-Control-Allow-Methods — which HTTP methods are permitted
  • Access-Control-Allow-Headers — which request headers are allowed

Why It Works — And Why curl Doesn't See the Problem

Here's the insight that makes everything click: CORS is enforced by the browser, not the server.

The server's only job is to declare its policy via response headers. The browser reads those headers and decides whether to expose the response to the requesting script. The server has no power to enforce CORS itself.

This has two important implications:

1. curl, Postman, and server-to-server calls are not affected by CORS. These clients don't implement the Same-Origin Policy — they're not browsers. If you test your API with curl and CORS "doesn't seem to matter," that's because it doesn't apply there. Non-browser clients see the raw response regardless of CORS headers.

2. CORS is not authentication. CORS protects a specific attack vector — malicious websites hijacking a user's browser session. It does nothing to stop:

  • Automated scripts making direct HTTP requests
  • Mobile apps consuming your API
  • Attackers with stolen tokens calling your API from anywhere

Setting Access-Control-Allow-Origin: * removes cross-browser-origin protections. But your API still needs proper authentication for everything else.

Implementation Scheme — What Actually Happens

There are two types of cross-origin requests, and the browser handles them differently:

Simple requests (GET, POST with basic headers) go through one round trip:

[Browser: evil.com tab]
    |
    | GET /api/account + Origin: https://evil.com
    v
[Your server]
    |
    | 200 OK + Access-Control-Allow-Origin: https://myapp.com
    v
[Browser checks: does evil.com match allowed origins?]
    |
    | NO → blocks the script from reading the response
    v
[evil.com's JS gets an error — the data never reaches it]

Non-simple requests (PUT, DELETE, custom headers, JSON bodies) trigger a preflight:

[Browser: myapp.com tab]
    |
    | OPTIONS /api/data
    | + Origin: https://myapp.com
    | + Access-Control-Request-Method: DELETE
    v
[Your server]
    |
    | 200 OK
    | + Access-Control-Allow-Origin: https://myapp.com
    | + Access-Control-Allow-Methods: GET, POST, DELETE
    v
[Browser: preflight approved, sending actual request]
    |
    | DELETE /api/data + Origin: https://myapp.com
    v
[Your server processes the request]
    |
    | 200 OK
    v
[Browser: exposes response to myapp.com's script ✓]

The preflight is why you sometimes see OPTIONS requests in your network tab before the real request goes through. It's the browser asking permission before committing.

Configuring CORS Correctly

The solution to CORS errors is almost never "disable CORS." It's "configure your allowed origins properly."

Be explicit about which origins you trust. A list like ["https://myapp.com", "https://admin.myapp.com"] is much better than *. Use environment variables so your dev and production configs can differ:

// Example: Express (Node.js)
app.use(cors({
  origin: process.env.ALLOWED_ORIGINS.split(','), // from env
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

Development vs production. In development you probably need http://localhost:3000 in your allowed origins. In production, that should be removed. Keep these in separate config values, not hardcoded.

# Development
Access-Control-Allow-Origin: http://localhost:3000

# Production
Access-Control-Allow-Origin: https://myproductionapp.com

Use a reverse proxy for local development. If you find yourself fighting with CORS during local development, consider having your dev server proxy API requests. The browser sees requests going to the same origin as your frontend; the proxy forwards them to the API. This matches production topology better anyway.

When Wildcard Is Actually Fine

Access-Control-Allow-Origin: * is legitimate in specific contexts:

  • Public APIs designed for open consumption (weather data, public transit APIs)
  • CDN-hosted assets: fonts, JavaScript libraries, CSS
  • Open data services where no authentication exists

The rule: wildcard is only safe when the endpoint requires no authentication and returns no private data. The moment an endpoint is session-aware or returns user-specific data, wildcard opens a real attack surface.

Summary

CORS exists because browsers need to protect users from malicious sites hijacking their sessions. Disabling it removes a security layer that's protecting your users, not annoying them.

The two things to remember: configure allowed origins explicitly per environment, and understand that curl bypassing CORS doesn't mean your API is safe — it means CORS was never the right tool for that threat.

Related posts in this series: