Security Best Practices

Difficulty: Intermediate

Building a secure Node.js application goes far beyond just authentication. Security is a multi-layered discipline, and Express applications face numerous attack vectors including cross-site scripting (XSS), cross-site request forgery (CSRF), injection attacks, brute-force attempts, and information leakage through headers. A defense-in-depth approach uses multiple security layers so that if one fails, others still protect the application.

Helmet is an essential security middleware that sets various HTTP headers to protect against common web vulnerabilities. It disables the X-Powered-By header (which tells attackers you use Express), sets Content-Security-Policy to prevent XSS, enables X-Content-Type-Options to stop MIME sniffing, and configures Strict-Transport-Security to enforce HTTPS. A single `app.use(helmet())` call applies sensible defaults that address over a dozen security headers.

Cross-Origin Resource Sharing (CORS) controls which domains can make requests to your API. By default, browsers block cross-origin requests for security. The `cors` middleware lets you configure allowed origins, methods, and headers. In development, you might allow all origins, but in production, you should whitelist only your frontend domain. Misconfigured CORS is a common vulnerability - setting `origin: '*'` with `credentials: true` is dangerous because it allows any site to make authenticated requests to your API.

Rate limiting protects against brute-force attacks and denial-of-service attempts. The `express-rate-limit` middleware restricts how many requests a client can make within a time window. For authentication endpoints like login and password reset, use aggressive limits (e.g., 5 attempts per 15 minutes). For general API endpoints, use more lenient limits (e.g., 100 requests per minute). This prevents attackers from guessing passwords or overwhelming your server.

Input sanitization prevents injection attacks by cleaning user input before processing it. Never trust data from the client - validate and sanitize everything. Use libraries like `express-validator` or `joi` for request validation, and `xss-clean` or `DOMPurify` (server-side) to strip HTML/script tags from user input. For database queries, always use parameterized queries or ORM methods instead of string concatenation to prevent SQL injection. With Prisma or Mongoose, the ORM handles parameterization automatically, but raw queries need explicit parameter binding.

Code examples

Helmet and CORS Configuration

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

const app = express();

// Security headers
app.use(helmet());

// CORS configuration
const corsOptions = {
  origin: process.env.NODE_ENV === 'production'
    ? ['https://myapp.com', 'https://www.myapp.com']
    : ['http://localhost:5173'],
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 86400 // Cache preflight for 24 hours
};
app.use(cors(corsOptions));

console.log('Helmet headers applied');
console.log('CORS origin:', corsOptions.origin);
console.log('Credentials allowed:', corsOptions.credentials);

Helmet sets security headers with one line. CORS is configured differently for development and production - in dev, the Vite dev server origin is allowed; in production, only the real domain. Credentials: true allows cookies/auth headers to be sent cross-origin.

Rate Limiting for API Protection

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
  standardHeaders: true, // Return rate limit info in headers
  legacyHeaders: false,
  message: { error: 'Too many requests, please try again later' }
});

// Strict limit for auth endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // Only 5 attempts
  message: { error: 'Too many login attempts, try again in 15 minutes' },
  skipSuccessfulRequests: true // Do not count successful logins
});

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

console.log('API limiter: 100 req / 15 min');
console.log('Auth limiter: 5 req / 15 min');
console.log('Successful requests skipped:', true);

Two rate limiters are created with different thresholds. The auth limiter is much stricter and uses skipSuccessfulRequests to only count failed attempts. The standardHeaders option adds RateLimit-* headers to responses so clients know their remaining quota.

Input Validation and Sanitization

const { body, validationResult } = require('express-validator');

// Validation middleware chain
const registerValidation = [
  body('email')
    .isEmail().withMessage('Invalid email')
    .normalizeEmail(),
  body('password')
    .isLength({ min: 8 }).withMessage('Password must be at least 8 characters')
    .matches(/\d/).withMessage('Password must contain a number')
    .matches(/[A-Z]/).withMessage('Password must contain an uppercase letter'),
  body('name')
    .trim()
    .isLength({ min: 2, max: 50 }).withMessage('Name must be 2-50 characters')
    .escape() // Prevent XSS by escaping HTML entities
];

// Validation error handler
function handleValidationErrors(req, res, next) {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({
      error: 'Validation failed',
      details: errors.array().map(e => ({ field: e.path, message: e.msg }))
    });
  }
  next();
}

// Usage:
// app.post('/register', registerValidation, handleValidationErrors, registerHandler);

console.log('Validation rules:', registerValidation.length, 'fields');
console.log('Each field has chained validators and sanitizers');

express-validator chains validation and sanitization rules. normalizeEmail() standardizes email format, trim() removes whitespace, escape() converts HTML characters to entities (preventing XSS). The handleValidationErrors middleware collects all errors and returns them in a structured format.

Key points

Concepts covered

helmet, CORS, rate limiting, input sanitization, HTTPS, express-rate-limit, XSS, CSRF