100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Node.js & Express Backend
30 minintermediate

CORS, Helmet, Rate Limiting and CSRF

CORS, Helmet, Rate Limiting and CSRF

A production Express API faces a constant stream of security threats: cross-origin requests from unauthorised domains (CORS), missing security headers that leak server information (Helmet), brute-force and denial-of-service attacks (rate limiting), and forged state-changing requests from malicious sites (CSRF). Each threat requires a distinct defence. This lesson covers the four most essential security middleware layers for any production Express application.

Analogy🏏Cricket
Think of it like cricket: The stadium's security staff have four different jobs. The CORS officer checks which teams are allowed in which gates — only pre-approved visitor support groups get through. The Helmet officer ensures players wear the right protective gear and no sensitive information is visible on their kit. The rate-limiting steward prevents a fan from buying all the tickets in one transaction. The CSRF guard checks that ticket requests come from the official booking system, not a counterfeit website. Virat Kohli's stadium operates with all four protections simultaneously — your API should too.

CORS — Cross-Origin Resource Sharing

Browsers enforce the Same-Origin Policy: JavaScript on domain-a.com cannot make fetch requests to domain-b.com unless the server explicitly permits it. CORS headers on the response tell the browser which origins, methods, and headers are allowed. Express's cors package makes this configurable. Misconfigured CORS (e.g., origin: '*' with credentials: true) is a critical security vulnerability.

bash
npm install cors
javascript
const cors = require('cors');

// Allow specific origins
const allowedOrigins = [
  'https://myapp.com',
  'https://admin.myapp.com',
  process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : null
].filter(Boolean);

app.use(cors({
  origin: (origin, callback) => {
    // Allow requests with no origin (curl, Postman, mobile apps)
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('CORS: origin not allowed'));
    }
  },
  methods:          ['GET','POST','PUT','PATCH','DELETE','OPTIONS'],
  allowedHeaders:   ['Content-Type', 'Authorization'],
  credentials:      true,   // Allow cookies (required for refresh token cookies)
  maxAge:           86400    // Cache preflight for 24 hours
}));
Analogy🏏Cricket
Think of it like cricket: CORS is the ICC's pre-approved match schedule. Only teams that are on the official fixture list (allowedOrigins) are allowed to play. A random club team turning up and claiming to be Australia (a browser script from an unknown origin) is turned away at the gate. The 'credentials: true' setting is like allowing each team to bring their own kit bag (cookies) through the gate.

Helmet — Security Headers

Helmet is a collection of small middleware functions that set HTTP response headers to protect against well-known vulnerabilities: Content-Security-Policy prevents XSS, X-Frame-Options prevents clickjacking, Strict-Transport-Security enforces HTTPS, X-Content-Type-Options prevents MIME sniffing. In Express 5+, Helmet is no longer bundled and must be installed separately.

bash
npm install helmet
javascript
const helmet = require('helmet');

// Use Helmet with a sensible default configuration
app.use(helmet());

// Customise Content Security Policy for your app
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc:  ["'self'"],
    scriptSrc:   ["'self'", "https://cdn.jsdelivr.net"],
    styleSrc:    ["'self'", "'unsafe-inline'"],
    imgSrc:      ["'self'", "data:", "https://res.cloudinary.com"],
    connectSrc:  ["'self'", "https://api.myapp.com"],
    frameAncestors: ["'none'"]   // Prevents clickjacking
  }
}));

// Helmet sets these headers automatically:
// X-DNS-Prefetch-Control: off
// X-Frame-Options: SAMEORIGIN
// X-Content-Type-Options: nosniff
// Referrer-Policy: no-referrer
// Strict-Transport-Security: max-age=15552000
// X-Permitted-Cross-Domain-Policies: none
Analogy🏏Cricket
Think of it like cricket: Helmet is the standardised player equipment rulebook. Every player must wear a helmet, guards, and gloves — no exceptions. These aren't optional; they're mandatory baseline protections. A batter who refuses to wear a helmet (missing Helmet middleware) is allowed on the field but is one short delivery away from a preventable injury. The ICC mandates protective equipment; the OWASP Top 10 mandates security headers.

Rate Limiting

Rate limiting restricts how many requests a client can make within a time window. Without it, your API is vulnerable to brute-force attacks on login endpoints, credential stuffing, and application-layer denial-of-service. The express-rate-limit package provides a flexible in-process store. For production with multiple server instances, use a Redis-backed store (rate-limit-redis).

Analogy🏏Cricket
🏏 Think of it like cricket: A ticket gate caps how many entry attempts one person can make in a window — try the turnstile a hundred times a minute with different stubs and security steps in, because that pattern is someone testing stolen tickets, not a genuine fan. Just as capping attempts per window stops ticket-fraud and crushes at the gate, rate limiting restricts how many requests a client can make in a time window, defending against brute-force login attempts, credential stuffing, and application-layer denial-of-service. Just as a single gate keeps its own tally, express-rate-limit provides a flexible in-process store for one instance. Just as a stadium with many gates needs a shared central count so a fraudster cannot simply switch entrances, a deployment with multiple server instances needs a Redis-backed store (rate-limit-redis) to enforce one shared limit. The payoff: rate limiting on sensitive routes turns high-volume automated abuse into a quickly-throttled dead end.
bash
npm install express-rate-limit
javascript
const rateLimit = require('express-rate-limit');

// General API rate limit
const apiLimiter = rateLimit({
  windowMs:          15 * 60 * 1000,  // 15 minutes
  max:               100,              // 100 requests per window per IP
  standardHeaders:   true,             // Return rate limit info in RateLimit-* headers
  legacyHeaders:     false,
  message:           { error: 'Too many requests, please try again later.' }
});

// Stricter limit for auth endpoints
const authLimiter = rateLimit({
  windowMs:  15 * 60 * 1000,
  max:       10,   // Only 10 login attempts per 15 min
  message:   { error: 'Too many login attempts' }
});

app.use('/api/', apiLimiter);
app.use('/auth/login',   authLimiter);
app.use('/auth/register', authLimiter);

In production with multiple Node.js instances (PM2 cluster, Kubernetes), store rate limit counters in Redis so limits are shared across all processes. The rate-limit-redis package provides a Redis-compatible store for express-rate-limit.

How CSRF Protection Works

CSRF (Cross-Site Request Forgery) attacks trick an authenticated user's browser into making a state-changing request to your API. The browser automatically sends cookies on cross-origin requests, so if your API uses cookie-based sessions, a malicious form on evil.com can POST to your /transfer endpoint with the victim's session cookie. The Double Submit Cookie pattern and the csurf library (deprecated) or csrf-csrf defend against this.

Analogy🏏Cricket
🏏 Think of it like cricket: Imagine a member's club card that automatically authorises any transaction it is presented for — a con artist could slip the member a form that, once touched, silently charges the member's account, because the card vouches for whoever waves it. Just as the card is trusted regardless of who actually initiated the request, the browser automatically attaches your session cookie to cross-origin requests, so a malicious form on evil.com can POST to your /transfer endpoint riding the victim's cookie — that is CSRF. Just as the club defends by demanding a second, single-use token printed only on genuine club stationery that a forger cannot reproduce, the Double Submit Cookie pattern requires a matching token the attacker's site cannot read. Just as clubs eventually retire an outdated verification scheme, note the csurf library is now deprecated. The payoff: requiring an unguessable token on state-changing, cookie-authenticated requests blocks forged cross-site actions.
bash
npm install csrf-csrf
javascript
const { doubleCsrf } = require('csrf-csrf');

const { generateToken, doubleCsrfProtection } = doubleCsrf({
  getSecret:    () => process.env.CSRF_SECRET,
  cookieName:   '__Host-psifi.x-csrf-token',
  cookieOptions: { sameSite: 'strict', secure: true, httpOnly: true },
  size:          64,
  getTokenFromRequest: (req) => req.headers['x-csrf-token']
});

// Provide CSRF token to the frontend
app.get('/api/csrf-token', (req, res) => {
  res.json({ csrfToken: generateToken(req, res) });
});

// Protect all state-changing routes
app.use('/api', doubleCsrfProtection);

// Frontend: fetch the token on app load, attach it to every mutation request
// fetch('/api/csrf-token').then(r => r.json()).then(d => {
//   axios.defaults.headers.common['x-csrf-token'] = d.csrfToken;
// });

JWT-based APIs that use Authorization: Bearer <token> headers (not cookies) are inherently CSRF-safe because browsers do not automatically include the Authorization header in cross-origin requests. CSRF protection is only required when you use cookies for authentication.

Lesson 22 of 36
0% complete