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.
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
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.openerDOM 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
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
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
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, orPATCH - 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
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
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: *
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');
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
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();
});
Three rules:
- Use an explicit allowlist of trusted origins
- Never reflect the Origin header blindly
- 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)