CORS Best Practices: Configuration, Security, and Common Mistakes

TL;DR

Whitelist specific origins rather than using wildcards. Access-Control-Allow-Origin: * with credentials is rejected by browsers anyway, but the intent alone is wrong. The trickier risk is dynamic origin reflection: an origin validator that reflects back whatever it receives lets any site access your API with user cookies. CORS protects users, not your server endpoints. Attackers with curl bypass it entirely.

"CORS protects users, not servers. A misconfigured policy lets attackers weaponize your users' browsers against your own API."

What CORS Actually Protects

CORS (Cross-Origin Resource Sharing) is a browser security feature that prevents malicious websites from accessing data from other sites.

CORS protects users, not your API. CORS is enforced by browsers. Attackers can still call your API directly with tools like curl. CORS prevents a malicious site from using a victim's browser to steal their data from your API.

Best Practice 1: Whitelist Specific Origins 3 min

Wildcards are fine for public static files. For any API that handles authentication or user data, they're not:

Express CORS configuration
import cors from 'cors';

// WRONG: Allows any origin
app.use(cors({ origin: '*' }));

// WRONG: Allows any origin with credentials (browser blocks this anyway)
app.use(cors({ origin: '*', credentials: true }));

// CORRECT: Whitelist specific origins
const allowedOrigins = [
  'https://yourdomain.com',
  'https://app.yourdomain.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, etc.)
    if (!origin) return callback(null, true);

    if (allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true,
}));

Best Practice 2: Avoid Dynamic Origin Reflection 2 min

This is the one that actually gets apps compromised. Reflecting back whatever origin is sent looks like a flexible whitelist. It isn't:

Dangerous origin reflection
// DANGEROUS: Reflects any origin
app.use(cors({
  origin: (origin, callback) => {
    callback(null, origin); // Reflects attacker's origin!
  },
  credentials: true,
}));

// This allows ANY site to access your API with cookies
// Attacker's site can steal user data

// CORRECT: Validate against whitelist
app.use(cors({
  origin: (origin, callback) => {
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, origin);
    } else {
      callback(new Error('Not allowed'));
    }
  },
  credentials: true,
}));

Best Practice 3: Configure Methods and Headers 2 min

Only allow the methods and headers you actually use. Permissive defaults here aren't dangerous by themselves, but they expand your attack surface for free:

Restrictive CORS configuration
app.use(cors({
  origin: allowedOrigins,
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE'], // Only methods you use
  allowedHeaders: ['Content-Type', 'Authorization'], // Only headers you need
  exposedHeaders: ['X-Request-Id'], // Headers client can read
  maxAge: 86400, // Cache preflight for 24 hours
}));

Best Practice 4: Handle Preflight Requests 3 min

Browsers send a preflight OPTIONS request before certain cross-origin requests. If you're not using a CORS middleware, you need to handle it yourself:

Manual preflight handling
// When not using cors middleware
app.options('*', (req, res) => {
  const origin = req.headers.origin;

  if (allowedOrigins.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    res.setHeader('Access-Control-Allow-Credentials', 'true');
    res.setHeader('Access-Control-Max-Age', '86400');
    res.status(204).end();
  } else {
    res.status(403).end();
  }
});

Best Practice 5: CORS with Cookies 3 min

Cookies need their own CORS settings on both sides of the request:

CORS with credentials
// Server: Allow credentials
app.use(cors({
  origin: 'https://app.yourdomain.com', // Must be specific, not *
  credentials: true, // Allow cookies
}));

// Client: Include credentials in fetch
fetch('https://api.yourdomain.com/user', {
  credentials: 'include', // Send cookies
});

// Cookie must also have proper settings
res.cookie('session', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'none', // Required for cross-origin
  domain: '.yourdomain.com', // Shared domain
});

Best Practice 6: Per-Route CORS 2 min

Different routes may need different CORS policies:

Route-specific CORS
import cors from 'cors';

// Public API: Allow any origin, no credentials
const publicCors = cors({
  origin: '*',
  methods: ['GET'],
});

// Private API: Strict origin, with credentials
const privateCors = cors({
  origin: allowedOrigins,
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
});

// Apply per route
app.get('/api/public/status', publicCors, statusHandler);
app.use('/api/user', privateCors, userRouter);
app.use('/api/admin', privateCors, adminRouter);

Common CORS Mistakes

MistakeRiskPrevention
origin: "*" with credentialsBrowser blocks (but intent is wrong)Whitelist specific origins
Reflecting any originCross-origin data theftValidate against whitelist
Trusting null originSandbox escape attacksNever allow null origin
Regex subdomain matchingBypass via evil.com.attacker.comUse exact matches
CORS on public filesUnnecessary (use CDN)CORS for API only

External Resources

For comprehensive CORS documentation and specifications, see the MDN CORS documentation, which covers browser behavior, preflight requests, and all CORS headers in detail.

When should I use Access-Control-Allow-Origin: *?

Only for truly public resources that don't require authentication and contain no sensitive data: weather APIs, static files, public documentation. Never with credentials.

Why does CORS not protect my API from hackers?

CORS is enforced by browsers only. Attackers can bypass it using curl, Postman, or their own servers. CORS protects users by preventing malicious sites from using their browser to access your API.

How do I allow multiple origins?

CORS only allows one origin in the header. Use a function to check the request origin against your whitelist and return that origin if allowed. See the examples above.

What is the null origin and should I allow it?

The null origin comes from sandboxed iframes, file:// URLs, and some redirects. Allowing it can enable attacks from sandboxed pages. Don't allow it unless you have a specific, understood reason.

Further Reading

Put these practices into action with our step-by-step guides.

Check Your CORS Configuration

Scan your API for CORS misconfigurations.

Best Practices

CORS Best Practices: Configuration, Security, and Common Mistakes