← ← Back to all posts

CORS Error: The Definitive Guide to Understanding, Fixing, and Avoiding Security Issues

2025-12-26 · Benja

CORS is not a server error but a browser security mechanism. Learning how to configure it correctly will prevent headaches in development and production, and help you build more secure APIs.

CORS Error: The Definitive Guide to Understanding, Fixing, and Avoiding Security Issues

What is CORS and why does it exist?

Practical analogy: Think of CORS as an invitation system for an event. Your frontend (the guest) wants to access resources from an API (the event). The browser (the security guard) checks whether the API issued a valid invitation (CORS headers). Without it, access is denied.

CORS (Cross-Origin Resource Sharing) is a security mechanism implemented by all modern browsers that allows web servers to explicitly declare which external origins are allowed to access their resources.

What counts as an “origin”?

An origin is defined by the unique combination of three components:

Component Example Does it change the origin?
Protocol (Scheme) http:// vs https:// ✅ Yes
Domain (Host) api.domain.com vs app.domain.com ✅ Yes
Port :3000 vs :8080 ✅ Yes

Examples of different origins:

// SAME ORIGIN
https://app.mydomain.com:443 → https://app.mydomain.com:443

// DIFFERENT ORIGINS
https://app.mydomain.com  → https://api.mydomain.com       // Different host
https://app.mydomain.com  → http://app.mydomain.com        // Different protocol
https://app.mydomain.com  → https://app.mydomain.com:8080  // Different port
https://app.mydomain.com  → https://mydomain.com           // Different subdomain

The “CORS error” is not a server error

Key insight: When you see a CORS error in the browser console, your server already responded (sometimes even with 200 OK). The browser is simply blocking your JavaScript from accessing that response for security reasons.

Typical request flow with CORS:

  1. Frontend: Your app at https://app.com calls https://api.com/data
  2. Browser: Detects a cross-origin request and adds Origin: https://app.com
  3. Server: Responds with data but without Access-Control-Allow-Origin: https://app.com
  4. Browser: Blocks access to the response → CORS error in the console

Why does it work in Postman/curl but not in the browser?

Tools like Postman, curl, or server-side scripts do not enforce the Same-Origin Policy. Only web browsers apply this restriction for security.

Common CORS error causes and diagnosis

1. Missing or incorrect Access-Control-Allow-Origin

The server must include this header with the exact frontend origin, or * (with limitations).

Correct configuration:

// For a specific origin
Access-Control-Allow-Origin: https://app.mydomain.com

// For multiple origins (requires backend logic)
// The server must validate Origin and reply with the appropriate value

2. Preflight request (OPTIONS) failure

The browser sends an OPTIONS request before “non-simple” requests:

Typical OPTIONS request:

OPTIONS /api/users HTTP/1.1
Host: api.mydomain.com
Origin: https://app.mydomain.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Authorization, Content-Type

Expected OPTIONS response:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.mydomain.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400  // Cache for 24 hours

3. Credentials (cookies/auth) misconfiguration

When you send cookies or use credentials: 'include' in fetch:

Incorrect configuration:

// Frontend
fetch('https://api.com/data', {
  credentials: 'include'  // Sends cookies
});

// Backend (ERROR - not compatible with credentials)
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true  // Contradiction

Correct configuration:

// Backend
Access-Control-Allow-Origin: https://app.mydomain.com  // Specific, NOT *
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: X-Auth-Token  // Custom headers visible to JS

4. Custom headers not allowed

Headers like X-API-Key or X-Custom-Header must be explicitly allowed:

Access-Control-Allow-Headers: Content-Type, Authorization, X-API-Key, X-Custom-Header

Practical solutions by environment and stack

Express.js (Node.js)

const express = require('express');
const cors = require('cors');

const app = express();

// Basic development setup
app.use(cors({
  origin: 'http://localhost:3000',
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With']
}));

// Production setup (allowlist)
const allowedOrigins = [
  'https://app.mydomain.com',
  'https://staging.mydomain.com'
];

app.use(cors({
  origin: function(origin, callback) {
    // Allow requests without Origin (e.g., mobile apps or curl)
    if (!origin) return callback(null, true);

    if (allowedOrigins.indexOf(origin) === -1) {
      const msg = `Origin ${origin} is not allowed by CORS`;
      return callback(new Error(msg), false);
    }
    return callback(null, true);
  },
  credentials: true
}));

Django (Python)

# settings.py
INSTALLED_APPS = [
    ...
    'corsheaders',
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',  # Must be before CommonMiddleware
    ...
]

# CORS configuration
CORS_ALLOWED_ORIGINS = [
    "https://app.mydomain.com",
    "https://staging.mydomain.com",
    "http://localhost:3000",
]

# Local development only
CORS_ALLOW_ALL_ORIGINS = True  # Development only

# Allow cookies
CORS_ALLOW_CREDENTIALS = True

# Allowed headers
CORS_ALLOW_HEADERS = [
    'content-type',
    'authorization',
    'x-csrftoken',
    'x-requested-with',
]

Nginx (reverse proxy) configuration

server {
    listen 80;
    server_name api.mydomain.com;

    location / {
        # CORS preflight
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' 'https://app.mydomain.com';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';
            add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
            add_header 'Access-Control-Max-Age' 1728000;
            add_header 'Content-Type' 'text/plain; charset=utf-8';
            add_header 'Content-Length' 0;
            return 204;
        }

        # Normal requests
        add_header 'Access-Control-Allow-Origin' 'https://app.mydomain.com' always;
        add_header 'Access-Control-Allow-Credentials' 'true' always;
        add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;

        # Proxy to your app
        proxy_pass http://localhost:8080;
    }
}

Development approach: Proxy in Vite/Webpack

// vite.config.js (Vite)
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3001',
        changeOrigin: true,
        secure: false,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
}

// Frontend calls /api/users -> Vite forwards to localhost:3001/users
// Same origin from the browser's perspective = fewer CORS problems

CORS and security: what you should know

Important: CORS is NOT an authentication or authorization mechanism. It only controls which frontends can read your API responses from a browser.

Risky myths about CORS

Myth: “If I configure CORS, my API is secure”

CORS only affects browsers. Anyone can call your API directly using curl, Postman, or scripts.

Reality: Security requires multiple layers

  • Authentication: JWT, OAuth, sessions
  • Authorization: RBAC, resource-level permissions
  • Validation: input sanitization, schema validation
  • Rate limiting: abuse protection
  • CORS: one additional layer

Recommended security patterns

API keys for native/mobile apps

For non-browser clients, use API keys with appropriate, client-specific rate limiting.

Strict CORS in production

Use an allowlist of origins. Do not use * in production when credentials are involved.

Rotating refresh tokens

For SPAs, implement refresh token rotation to reduce the impact of token theft.

Advanced cases and edge cases

WebSockets and CORS

WebSockets are not subject to CORS, but many servers still validate the Origin header manually.

// Socket.io configuration
const io = require('socket.io')(server, {
  cors: {
    origin: "https://app.mydomain.com",
    methods: ["GET", "POST"],
    credentials: true
  }
});

CDNs and CORS for static assets

For web fonts, images, or other assets hosted on a CDN:

# AWS S3 CORS configuration
[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["GET"],
    "AllowedOrigins": ["https://app.mydomain.com"],
    "ExposeHeaders": []
  }
]

Microservices and internal CORS

In microservice architectures, consider:

  • An API gateway handling CORS once
  • Internal service-to-service communication without CORS concerns
  • Custom headers for tracing across services

CORS implementation checklist

Extended FAQ

How do I debug CORS issues in the browser?

In DevTools → Network:

  1. Filter by “OPTIONS” to find preflight requests
  2. Inspect request and response headers
  3. Confirm Access-Control-Allow-Origin matches exactly
  4. Use “Copy as cURL” to test outside the browser

Can I use wildcards for subdomains?

Yes, but with limitations:

// VALID for specific subdomain patterns (implementation-dependent)
Access-Control-Allow-Origin: https://*.mydomain.com

// INVALID - do not mix protocols
Access-Control-Allow-Origin: *://app.mydomain.com

// BEST PRACTICE: Validate Origin dynamically on the backend
const allowedPattern = /^https:\/\/(app|staging|api)\.mydomain\.com$/;

What about redirects (301/302)?

Redirects can drop CORS headers. Typical fixes:

  • Configure CORS on the server issuing the redirect
  • Avoid redirects in APIs; return appropriate status codes
  • In Nginx/Apache, ensure CORS headers are set for all responses

Does CORS affect Server-Side Rendering (SSR)?

No, because SSR runs on the server. However:

  • Client-side calls during hydration are still subject to CORS
  • Consider API routes in your framework (Next.js, Nuxt) to reduce CORS surface area

Comments

0 comments

Leave a comment

It will appear once it is approved.

No comments yet.