CORS & Security Headers

Difficulty: Intermediate

Question

What is CORS and why is it needed? How do you secure a Node.js API with proper headers?

Answer

CORS (Cross-Origin Resource Sharing) is a browser security mechanism that restricts web pages from making requests to a different domain than the one that served the page. When your React app on localhost:5173 calls your API on localhost:3000, the browser blocks it by default because the origins differ.

The server must explicitly allow cross-origin requests by setting Access-Control-Allow-Origin and related headers. The cors middleware simplifies this, but understanding the underlying preflight mechanism is important.

Before a cross-origin request with custom headers or non-simple methods (PUT, DELETE), the browser sends an OPTIONS preflight request. The server must respond with the allowed methods, headers, and origins. Only then does the browser send the actual request.

Helmet is a security middleware that sets various HTTP headers to protect against common attacks: XSS (Content-Security-Policy), clickjacking (X-Frame-Options), MIME sniffing (X-Content-Type-Options), and more. It should be used on every production Express app.

Code examples

CORS Configuration

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

// Simple: allow all origins (development only!)
app.use(cors());

// Production: specific origins
app.use(cors({
  origin: ['https://myapp.com', 'https://admin.myapp.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,      // Allow cookies
  maxAge: 86400,          // Cache preflight for 24 hours
}));

// Dynamic origin based on whitelist
const allowedOrigins = process.env.ALLOWED_ORIGINS.split(',');
app.use(cors({
  origin: (origin, callback) => {
    // Allow requests with no origin (mobile apps, Postman)
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true,
}));

Never use cors() with no options in production. Whitelist specific origins. Enable credentials only if your app uses cookies or Authorization headers.

Helmet Security Headers

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

// Apply all default security headers
app.use(helmet());

// Or configure individually
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'unsafe-inline'"],
      styleSrc: ["'self'", "'unsafe-inline'", 'fonts.googleapis.com'],
      imgSrc: ["'self'", 'data:', '*.amazonaws.com'],
    },
  },
  crossOriginEmbedderPolicy: false, // Needed for loading external images
}));

// What helmet sets:
// X-Content-Type-Options: nosniff (prevents MIME sniffing)
// X-Frame-Options: SAMEORIGIN (prevents clickjacking)
// X-XSS-Protection: 0 (modern CSP replaces this)
// Strict-Transport-Security: max-age=... (forces HTTPS)
// Content-Security-Policy: ... (controls resource loading)

console.log('Security headers enabled');

Helmet sets 11+ security headers with sensible defaults. Customize Content-Security-Policy based on your app's needs.

Manual CORS Implementation

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

// Understanding what the cors middleware does internally
app.use((req, res, next) => {
  const origin = req.headers.origin;
  const allowed = ['https://myapp.com'];

  if (allowed.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
    res.setHeader('Access-Control-Allow-Credentials', 'true');
  }

  // Handle preflight
  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.sendStatus(204); // No content
  }

  next();
});

// Custom security headers
app.use((req, res, next) => {
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('X-Frame-Options', 'DENY');
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
  next();
});

CORS works by setting response headers. The browser checks these headers to decide whether to allow the response. OPTIONS preflight is handled separately.

Key points

Concepts covered

CORS, Helmet, Same-Origin Policy, Security Headers, XSS