Express.js Securing Express Apps
A working Express application is not automatically a secure one. Security requires deliberate choices: the right HTTP headers, rate limiting to block abuse, input sanitization to stop injections, and CORS configuration to control who can call your API. This topic covers the most important security layers every production Express app needs, with ready-to-use code for each one.
Security Layers Overview
Incoming Request
│
▼
┌─────────────────────────────────────────────────────┐
│ Layer 1: Helmet — sets security HTTP headers │
├─────────────────────────────────────────────────────┤
│ Layer 2: CORS — controls allowed origins │
├─────────────────────────────────────────────────────┤
│ Layer 3: Rate Limiting — blocks brute force │
├─────────────────────────────────────────────────────┤
│ Layer 4: Input Sanitization — blocks injections │
├─────────────────────────────────────────────────────┤
│ Layer 5: HTTPS — encrypts data in transit │
└─────────────────────────────────────────────────────┘
│
▼
Your Routes & Handlers (safe zone)
Layer 1: Helmet — Security HTTP Headers
Helmet adds HTTP response headers that protect against common browser-based attacks. Without these headers, browsers may expose your app to cross-site scripting (XSS), clickjacking, and other vulnerabilities.
npm install helmet
const helmet = require('helmet');
app.use(helmet()); // Adds 11 security headers automatically
Headers Helmet sets for you:
┌──────────────────────────────────────────────────────────────────┐ │ Headers Added by Helmet │ ├──────────────────────────────┬───────────────────────────────────┤ │ X-Content-Type-Options │ Prevents MIME sniffing │ │ X-Frame-Options │ Blocks clickjacking (iframe) │ │ X-XSS-Protection │ Enables browser XSS filter │ │ Strict-Transport-Security │ Forces HTTPS │ │ Content-Security-Policy │ Controls resource loading rules │ │ Referrer-Policy │ Controls referrer info sent │ └──────────────────────────────┴───────────────────────────────────┘
Layer 2: CORS — Cross-Origin Resource Sharing
Browsers block JavaScript from calling APIs on a different domain by default. CORS (Cross-Origin Resource Sharing) tells the browser which domains your API allows. Without CORS configuration, your React or Vue frontend cannot call your Express API.
npm install cors
const cors = require('cors');
// Allow only your specific frontend
app.use(cors({
origin: process.env.ALLOWED_ORIGIN || 'http://localhost:5173',
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true // Allow cookies to be sent cross-origin
}));
Never use origin: '*' (allow all) in production for APIs that handle authenticated data. Always specify the exact frontend domain.
CORS Configuration Matrix: ┌──────────────────────────────────────────────────────┐ │ Situation │ origin setting │ ├───────────────────────┼──────────────────────────────┤ │ Public API, read-only │ '*' (all origins OK) │ │ Frontend + backend │ 'https://yourapp.com' │ │ Multiple frontends │ ['https://a.com','https://b']│ │ Dynamic whitelist │ function(origin, callback) │ └───────────────────────┴──────────────────────────────┘
Layer 3: Rate Limiting
Rate limiting restricts how many requests a single IP address can make in a given time window. Without it, attackers can try thousands of password combinations per second (brute force) or flood your server with traffic (denial of service).
npm install express-rate-limit
const rateLimit = require('express-rate-limit');
// General API rate limit
const generalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window per IP
standardHeaders: true,
legacyHeaders: false,
message: {
error: 'Too many requests from this IP. Please try again in 15 minutes.'
}
});
// Strict limiter for auth routes
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 10, // Only 10 login attempts per 15 minutes
message: { error: 'Too many login attempts. Please try again later.' }
});
app.use('/api', generalLimiter);
app.use('/api/auth/login', authLimiter);
app.use('/api/auth/signup', authLimiter);
Layer 4: Input Sanitization
Attackers send malicious data in request bodies to manipulate databases or execute scripts. Sanitization strips or escapes dangerous content before it reaches your logic.
Prevent NoSQL Injection with express-mongo-sanitize
Without sanitization, an attacker can log in as anyone by sending { "$gt": "" } as the password — a MongoDB operator that always evaluates to true:
npm install express-mongo-sanitize
const mongoSanitize = require('express-mongo-sanitize');
// Removes any keys that start with $ from req.body, req.query, req.params
app.use(mongoSanitize());
Prevent XSS Attacks with xss-clean
npm install xss-clean
const xss = require('xss-clean');
// Sanitizes user input against XSS attacks
app.use(xss());
Limit Request Body Size
Large request bodies can exhaust server memory. Limit them:
app.use(express.json({ limit: '10kb' })); // JSON bodies max 10KB
app.use(express.urlencoded({ extended: true, limit: '10kb' }));
Layer 5: HTTPS in Production
HTTPS encrypts data between the browser and your server. Without it, passwords and tokens sent over the network are visible to anyone intercepting the traffic. In production, configure HTTPS at the infrastructure level (your hosting provider's load balancer or nginx), not in Express itself.
During development, redirect HTTP to HTTPS conditionally:
// Redirect HTTP to HTTPS in production
if (process.env.NODE_ENV === 'production') {
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https') {
return res.redirect(`https://${req.header('host')}${req.url}`);
}
next();
});
}
Additional Security Practices
Hide the X-Powered-By Header
Express adds an X-Powered-By: Express header by default. This tells attackers which framework you use. Helmet removes it automatically, but you can also disable it manually:
app.disable('x-powered-by');
Parameter Pollution Prevention
npm install hpp
const hpp = require('hpp');
app.use(hpp()); // Prevents HTTP parameter pollution attacks
Validate Input with express-validator
npm install express-validator
const { body, validationResult } = require('express-validator');
app.post('/api/signup',
// Validation rules
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 8 }).trim(),
body('name').notEmpty().trim().escape(),
// Handler
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Process validated input
res.json({ message: 'Valid data received' });
}
);
Complete Security Setup in app.js
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const mongoSanitize = require('express-mongo-sanitize');
const xss = require('xss-clean');
const hpp = require('hpp');
const app = express();
// 1. Security headers
app.use(helmet());
// 2. CORS
app.use(cors({ origin: process.env.ALLOWED_ORIGIN, credentials: true }));
// 3. Body parsing with size limit
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true, limit: '10kb' }));
// 4. Rate limiting
app.use('/api', rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
// 5. Input sanitization
app.use(mongoSanitize());
app.use(xss());
app.use(hpp());
// Your routes go here
app.use('/api/users', userRouter);
Security Checklist
┌────────────────────────────────────────────────────────────────┐ │ Production Security Checklist │ ├────────────────────────────────────────────┬───────────────────┤ │ Helmet headers installed │ ✓ or ✗ │ │ CORS configured with specific origin │ ✓ or ✗ │ │ Rate limiting on all routes │ ✓ or ✗ │ │ Strict rate limit on auth routes │ ✓ or ✗ │ │ NoSQL injection prevention │ ✓ or ✗ │ │ XSS protection │ ✓ or ✗ │ │ Request body size limited │ ✓ or ✗ │ │ Passwords hashed with bcrypt │ ✓ or ✗ │ │ Secrets in environment variables │ ✓ or ✗ │ │ HTTPS enabled in production │ ✓ or ✗ │ │ x-powered-by header removed │ ✓ or ✗ │ └────────────────────────────────────────────┴───────────────────┘
Summary
Securing an Express app requires multiple layers working together. Helmet sets protective HTTP headers with one line. CORS restricts which domains can call your API — always name specific origins in production. Rate limiting with express-rate-limit blocks brute force attacks by capping requests per IP. Sanitize inputs with express-mongo-sanitize and xss-clean to prevent NoSQL injection and cross-site scripting. Limit request body sizes to prevent memory exhaustion. Validate all incoming data with express-validator before processing. Always use HTTPS in production and store every secret in environment variables. Apply all these measures together — security works through layers, not a single defense.
