CORS: Cross-Origin Resource Sharing Explained

The same-origin policy is a fundamental security concept in web browsers. It prevents JavaScript running on one origin (protocol + domain + port) from accessing resources on a different origin....

Key Insights

  • CORS is a security mechanism that allows servers to explicitly grant cross-origin access, overriding the browser’s same-origin policy that blocks requests between different domains by default.
  • Browsers automatically send preflight OPTIONS requests for complex cross-origin requests, and servers must respond with appropriate CORS headers before the actual request proceeds.
  • The most common CORS mistake is using a wildcard origin (*) with credentials—this combination is explicitly forbidden and will always fail.

What is CORS and Why It Exists

The same-origin policy is a fundamental security concept in web browsers. It prevents JavaScript running on one origin (protocol + domain + port) from accessing resources on a different origin. Without this policy, a malicious website could make authenticated requests to your bank’s API using your cookies, stealing your data.

An origin is defined by three components:

  • Protocol (http vs https)
  • Domain (example.com vs api.example.com)
  • Port (3000 vs 8080)

If any of these differ, the browser considers it a cross-origin request. This means http://localhost:3000 and http://localhost:8080 are different origins, as are https://example.com and https://api.example.com.

CORS (Cross-Origin Resource Sharing) is the mechanism that allows servers to relax this restriction selectively. When you build a frontend application that needs to call an API on a different domain, you’ll encounter CORS:

// Frontend running on http://localhost:3000
fetch('http://localhost:8080/api/users')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Error:', error));

// Browser console error:
// Access to fetch at 'http://localhost:8080/api/users' from origin 
// 'http://localhost:3000' has been blocked by CORS policy: No 
// 'Access-Control-Allow-Origin' header is present on the requested resource.

This error message is your browser protecting you. The API server must explicitly allow this cross-origin request.

How CORS Works: The Request-Response Flow

CORS operates through HTTP headers exchanged between the browser and server. The browser automatically adds an Origin header to cross-origin requests, and the server responds with Access-Control-* headers indicating what’s allowed.

Simple requests bypass preflight checks if they meet specific criteria:

  • Use GET, HEAD, or POST methods
  • Only use CORS-safe headers (Accept, Accept-Language, Content-Language, Content-Type)
  • Content-Type is limited to application/x-www-form-urlencoded, multipart/form-data, or text/plain

Preflight requests occur for anything more complex. The browser sends an OPTIONS request first to check permissions:

// This triggers a preflight because of the custom header
fetch('http://localhost:8080/api/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Custom-Header': 'value'
  },
  body: JSON.stringify({ name: 'John' })
});

// Browser automatically sends OPTIONS request first:
// OPTIONS /api/users HTTP/1.1
// Origin: http://localhost:3000
// Access-Control-Request-Method: POST
// Access-Control-Request-Headers: content-type, x-custom-header

// Server must respond with:
// HTTP/1.1 204 No Content
// Access-Control-Allow-Origin: http://localhost:3000
// Access-Control-Allow-Methods: POST, GET, OPTIONS
// Access-Control-Allow-Headers: content-type, x-custom-header
// Access-Control-Max-Age: 86400

Only after receiving approval does the browser send the actual POST request. The Access-Control-Max-Age header tells the browser how long to cache this preflight response, reducing overhead.

Configuring CORS on the Server

Setting CORS headers is straightforward, but the implementation varies by framework. Here’s a basic Node.js HTTP server example:

const http = require('http');

const server = http.createServer((req, res) => {
  // Set CORS headers for all responses
  res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  
  // Handle preflight requests
  if (req.method === 'OPTIONS') {
    res.writeHead(204);
    res.end();
    return;
  }
  
  // Your normal request handling
  if (req.url === '/api/users' && req.method === 'GET') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify([{ id: 1, name: 'John' }]));
  }
});

server.listen(8080);

For Express.js, use middleware:

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

// Manual CORS middleware
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  
  if (req.method === 'OPTIONS') {
    return res.sendStatus(204);
  }
  next();
});

app.get('/api/users', (req, res) => {
  res.json([{ id: 1, name: 'John' }]);
});

The cors npm package simplifies configuration:

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

// Simple usage - allows all origins (development only!)
app.use(cors());

// Production configuration
app.use(cors({
  origin: 'https://myapp.com',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 86400 // 24 hours
}));

When working with credentials (cookies, authorization headers), you must set Access-Control-Allow-Credentials: true and specify an exact origin—wildcards won’t work:

app.use(cors({
  origin: 'https://myapp.com',
  credentials: true
}));

// Frontend must also include credentials
fetch('http://localhost:8080/api/users', {
  credentials: 'include'
});

Common CORS Patterns and Solutions

A wildcard origin (*) allows any website to access your API, which is fine for public APIs but dangerous for authenticated endpoints:

// Public API - wildcard is acceptable
app.use('/api/public', cors({
  origin: '*'
}));

// Authenticated API - specify exact origins
app.use('/api/private', cors({
  origin: 'https://myapp.com',
  credentials: true
}));

For multiple allowed origins, implement dynamic validation:

const allowedOrigins = [
  'https://myapp.com',
  'https://staging.myapp.com',
  'http://localhost:3000'
];

app.use(cors({
  origin: function(origin, callback) {
    // Allow requests with no origin (mobile apps, Postman)
    if (!origin) return callback(null, true);
    
    if (allowedOrigins.indexOf(origin) === -1) {
      const msg = 'The CORS policy for this site does not allow access from the specified origin.';
      return callback(new Error(msg), false);
    }
    return callback(null, true);
  },
  credentials: true
}));

For environment-specific configuration:

const corsOptions = {
  origin: process.env.NODE_ENV === 'production'
    ? 'https://myapp.com'
    : ['http://localhost:3000', 'http://localhost:3001'],
  credentials: true
};

app.use(cors(corsOptions));

Troubleshooting CORS Errors

CORS errors appear in the browser console, not in network response bodies. The actual HTTP response might be successful (200 OK), but the browser blocks JavaScript access.

Common mistake #1: Missing CORS headers

// BEFORE - Missing headers
app.get('/api/users', (req, res) => {
  res.json([{ id: 1, name: 'John' }]);
});

// AFTER - Add CORS middleware
app.use(cors({ origin: 'http://localhost:3000' }));

app.get('/api/users', (req, res) => {
  res.json([{ id: 1, name: 'John' }]);
});

Common mistake #2: Wildcard with credentials

// WRONG - This will ALWAYS fail
app.use(cors({
  origin: '*',
  credentials: true
}));

// CORRECT - Specify exact origin
app.use(cors({
  origin: 'http://localhost:3000',
  credentials: true
}));

Common mistake #3: Not handling OPTIONS

// BEFORE - OPTIONS requests fail
app.post('/api/users', (req, res) => {
  res.json({ success: true });
});

// AFTER - Middleware handles OPTIONS automatically
app.use(cors());

app.post('/api/users', (req, res) => {
  res.json({ success: true });
});

To debug, check the Network tab in DevTools. Look for:

  • The preflight OPTIONS request (if applicable)
  • Response headers on both OPTIONS and actual request
  • The Access-Control-Allow-Origin header matching your origin exactly

Security Considerations and Best Practices

Never use wildcard origins for authenticated APIs. This allows any website to make credentialed requests to your API:

// INSECURE - Any site can access user data
app.use(cors({
  origin: '*',
  credentials: true // This combination is forbidden anyway
}));

// SECURE - Only your frontend can access
app.use(cors({
  origin: process.env.FRONTEND_URL,
  credentials: true
}));

Minimize allowed methods and headers:

// Too permissive
app.use(cors({
  origin: 'https://myapp.com',
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'],
  allowedHeaders: '*'
}));

// Better - only what you need
app.use(cors({
  origin: 'https://myapp.com',
  methods: ['GET', 'POST'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

Use environment variables for origin configuration:

const corsOptions = {
  origin: process.env.ALLOWED_ORIGINS?.split(',') || 'http://localhost:3000',
  credentials: true,
  maxAge: 86400
};

app.use(cors(corsOptions));

Remember that CORS is a browser security feature. It doesn’t prevent requests from servers, curl, or Postman. For API security, always implement proper authentication and authorization regardless of CORS configuration.

CORS protects users, not your server. Even with strict CORS policies, implement rate limiting, input validation, and authentication to secure your API endpoints.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.