DEV Community

Gorav Singal
Gorav Singal

Posted on

CORS & Same-Origin Policy — The Security Rule Every Developer Gets Wrong

Your fetch call fails. The console screams:

Access to fetch at 'https://api.example.com' from origin 'https://app.example.com' 
has been blocked by CORS policy.
Enter fullscreen mode Exit fullscreen mode

You Google it. Stack Overflow says add Access-Control-Allow-Origin: *. You do. It works. You move on.

But do you actually know what just happened? And more importantly — did you just open a security hole?


What Is an Origin?

Before we talk CORS, we need to define origin. An origin has three parts:

Part Example
Protocol https://
Host example.com
Port :443

All three must match for two URLs to be "same-origin."

https://example.com:443/page    ← Reference

https://example.com:443/other   ✅ Same origin (path doesn't matter)
http://example.com:443/page     ❌ Different protocol
https://api.example.com:443     ❌ Different host (subdomains count!)
https://example.com:8080        ❌ Different port
Enter fullscreen mode Exit fullscreen mode

One mismatch in any of the three — and the browser treats it as a completely different origin. No exceptions.


The Same-Origin Policy (SOP)

The Same-Origin Policy is the most fundamental security rule in the browser. Here's what it does:

Blocked by SOP

When JavaScript on origin A tries to read a response from origin B:

  • fetch() to another domain — response blocked
  • Reading an <iframe> DOM from another origin — blocked
  • Accessing window.opener DOM cross-origin — blocked

Allowed Cross-Origin

But SOP doesn't block everything:

  • <img> tags load cross-origin images — allowed
  • <script> tags load cross-origin scripts — allowed
  • <form> submissions go cross-origin — allowed

See the pattern? SOP blocks reading cross-origin responses. It does NOT block sending cross-origin requests.

That gap is exactly what CSRF exploits — and what CORS was built to solve.


Why CORS Exists

Modern web apps don't live on a single origin:

Frontend  →  app.example.com
API       →  api.example.com
CDN       →  cdn.example.com
Enter fullscreen mode Exit fullscreen mode

Three different origins. All blocked by SOP. If SOP blocked everything, the modern web wouldn't work.

CORS (Cross-Origin Resource Sharing) is the controlled exception. It's a protocol that lets the server say:

"I trust this specific origin. Let their JavaScript read my response."

The server opts in. The browser enforces it.


How Simple CORS Works

For simple requests — GET, HEAD, or POST with basic headers — no preflight is needed.

Step 1: Browser sends the request with an Origin header:

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

Step 2: Server responds with Access-Control-Allow-Origin:

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

Browser checks: does Origin match Access-Control-Allow-Origin?

  • Match → JavaScript can read the response
  • No match or missing → Browser blocks it. CORS error.

The request still went through. The server still processed it. But your code can't see the result.


Preflight Requests

Not all requests are simple. When you use:

  • PUT, DELETE, or PATCH
  • Custom headers like Authorization
  • Content-Type: application/json

The browser sends an OPTIONS request first — the preflight:

OPTIONS /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Authorization, Content-Type
Enter fullscreen mode Exit fullscreen mode

Server responds with what it allows:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400
Enter fullscreen mode Exit fullscreen mode

If everything checks out, then the browser sends the actual PUT request. Two round trips instead of one — but the server explicitly opted in to every method and header.

Max-Age: 86400 caches the preflight for 24 hours so subsequent requests skip the OPTIONS call.


3 Dangerous CORS Misconfigurations

This is where developers get burned.

1. Wildcard Origin

Access-Control-Allow-Origin: *
Enter fullscreen mode Exit fullscreen mode

Any website can read your API responses. For a truly public API with no auth — maybe fine. But * cannot be used with credentials (Access-Control-Allow-Credentials: true). The browser explicitly forbids it.

2. Reflecting the Origin (The Dangerous One)

// Server code — DON'T DO THIS
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
Enter fullscreen mode Exit fullscreen mode

Whatever origin the request comes from, the server echoes it back. evil.com sends a request? Server responds with Allow-Origin: evil.com. With credentials enabled.

That's a full CORS bypass. The attacker's page can now read authenticated responses from your API.

3. Trusting Null Origin

Access-Control-Allow-Origin: null
Enter fullscreen mode Exit fullscreen mode

Sandboxed iframes and file:// requests send Origin: null. If your server trusts null, an attacker can craft a sandboxed iframe that makes authenticated requests to your API.


The Fix: Explicit Allowlist

const ALLOWED_ORIGINS = [
  'https://app.example.com',
  'https://admin.example.com',
];

app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (ALLOWED_ORIGINS.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
    res.setHeader('Access-Control-Allow-Credentials', 'true');
  }
  next();
});
Enter fullscreen mode Exit fullscreen mode

Three rules:

  1. Use an explicit allowlist of trusted origins
  2. Never reflect the Origin header blindly
  3. Never trust null

TL;DR

Concept One-liner
Origin Protocol + Host + Port — all three must match
SOP Browser blocks JS from reading cross-origin responses
CORS Server opts in with Access-Control-Allow-Origin
Simple request GET/POST with basic headers — goes straight through
Preflight OPTIONS check before complex requests
Wildcard * works for public APIs, forbidden with credentials
Reflected origin Echoing back any origin = full CORS bypass
Null origin Sandboxed iframes exploit this — never trust it

Watch the Full Video

I made an animated video walking through all of this with visual diagrams — origins breaking apart, requests flowing between browser and server, preflight handshakes, and attack scenarios.

This is part of my Web Security series on GyanByte. Previous episodes covered Authentication, Authorization, XSS, and CSRF.

Next up: Content Security Policy (CSP) — the header that controls what your page can load, run, and connect to.


Have you been burned by a CORS misconfiguration? Drop your war story in the comments.

Top comments (0)