CORS Best Practices: Configuration, Security, and Common Mistakes

Share

TL;DR

The #1 CORS security best practice is to whitelist specific origins rather than using wildcards. Never use Access-Control-Allow-Origin: * with credentials. Be careful with dynamic origin reflection, and understand that CORS protects users, not your API. Proper CORS configuration prevents cross-origin data theft.

"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

Never use wildcards (*) for APIs that handle sensitive data:

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

Reflecting the Origin header back is dangerous:

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 HTTP methods and headers you actually use:

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 OPTIONS requests before certain cross-origin requests:

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 require specific CORS configuration:

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 do not require authentication and contain no sensitive data. Public APIs (like weather data), static files, or 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 null origin can enable attacks from sandboxed pages. Generally, do not allow it unless you have a specific need.

Check Your CORS Configuration

Scan your API for CORS misconfigurations.

Start Free Scan
Best Practices

CORS Best Practices: Configuration, Security, and Common Mistakes