CORS

Cross-Origin Resource Sharing - A security mechanism that controls how web pages can request resources from different domains.

CORS (Cross-Origin Resource Sharing) is a browser security feature that controls which websites can access your API. By default, browsers block requests from one origin to another - CORS headers tell browsers when to allow these cross-origin requests.

Why CORS Exists

Same-Origin Policy: Browsers block requests to different domains by default. This prevents malicious sites from stealing data via your logged-in sessions.

Origin = protocol + domain + port: https://example.com:443 is different from http://example.com or https://api.example.com

CORS relaxes this restriction: Server sends headers saying "I allow requests from these origins."

How CORS Works

Simple requests (GET, POST with simple headers):

  1. Browser sends request with Origin header
  2. Server responds with Access-Control-Allow-Origin
  3. Browser checks if origin is allowed
  4. If allowed, JavaScript gets response. If not, error.

Preflight requests (PUT, DELETE, custom headers):

  1. Browser sends OPTIONS request first
  2. Server responds with allowed methods and headers
  3. If allowed, browser sends actual request
  4. If not, actual request is blocked

CORS Headers Explained

HeaderPurposeExample
Access-Control-Allow-OriginWhich origins can accesshttps://myapp.com or *
Access-Control-Allow-MethodsAllowed HTTP methodsGET, POST, PUT, DELETE
Access-Control-Allow-HeadersAllowed request headersContent-Type, Authorization
Access-Control-Allow-CredentialsAllow cookies/authtrue
Access-Control-Max-AgeCache preflight (seconds)86400
Access-Control-Expose-HeadersHeaders JS can readX-Total-Count

Common CORS Configurations

Public API (no auth): Access-Control-Allow-Origin: * Anyone can call your API. Fine for public data.

Specific origins: Access-Control-Allow-Origin: https://myapp.com Only your app can call the API. Check origin dynamically for multiple allowed domains.

With credentials: Access-Control-Allow-Origin: https://myapp.com Access-Control-Allow-Credentials: true Required for cookies. Cannot use * with credentials.

Preflight Requests

Browsers send an OPTIONS preflight for:

  • Methods other than GET, HEAD, POST
  • Custom headers (like Authorization)
  • Content-Type other than form-urlencoded, multipart, text/plain

Optimize with Max-Age: Preflight adds latency. Cache preflight response: Access-Control-Max-Age: 86400 (24 hours)

Common CORS Errors

"No 'Access-Control-Allow-Origin' header": Server didn't include CORS headers. Add them.

"Credential is not supported if the CORS header is '*'": Using credentials with wildcard origin. Specify exact origin.

"Method not allowed": Server didn't list the method in Allow-Methods.

"Header not allowed": Server didn't list the header in Allow-Headers. Add Authorization if using JWT.

Security Best Practices

Never reflect Origin header blindly: Don't copy request Origin to response. Validates against whitelist.

Be specific with origins: * is fine for public APIs. Use explicit origins for private APIs.

Don't allow credentials with wildcard: Browsers block this anyway, but it signals misconfiguration.

Limit allowed methods: Only allow methods your API actually uses.

Validate origins server-side: CORS is browser-enforced. Server-to-server requests bypass it. Always authenticate.

CORS vs Authentication

CORS doesn't replace authentication: CORS controls browser requests. APIs are still accessible via curl, Postman, or server code. Always require authentication for protected endpoints.

CORS is not security by itself: It prevents malicious websites from using your users' sessions. It's not a general-purpose access control.

Common Mistakes

1. Adding CORS only to GET: OPTIONS preflight needs CORS headers too. Apply to all routes.

2. Forgetting Authorization header: If using JWT, add Authorization to Access-Control-Allow-Headers.

3. Wildcard with credentials: Access-Control-Allow-Origin: * with credentials fails. Use specific origin.

4. Not handling OPTIONS: Preflight requests need 200 response with CORS headers.

5. Development vs production: localhost:3000 in dev, but production origin in prod. Configure per environment.

Debugging CORS

Browser DevTools: Network tab shows CORS headers. Console shows specific error.

Check preflight: Filter network tab for OPTIONS requests. Check response headers.

Test without browser: curl ignores CORS. If curl works but browser doesn't, it's CORS.

Code Examples

CORS Configuration in Express

import cors from 'cors';

// Simple: Allow all origins
app.use(cors());

// Specific origins
const allowedOrigins = [
  'https://myapp.com',
  'https://staging.myapp.com',
  process.env.NODE_ENV === 'development' && 'http://localhost:3000'
].filter(Boolean);

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, origin);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 86400 // Cache preflight for 24 hours
}));

// Manual CORS (without middleware)
app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (allowedOrigins.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
  }
  res.setHeader('Access-Control-Allow-Credentials', 'true');

  if (req.method === 'OPTIONS') {
    res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization');
    res.setHeader('Access-Control-Max-Age', '86400');
    return res.status(200).end();
  }
  next();
});