Express Security Guide
Essential security hardening for Express.js applications covering HTTP headers, rate limiting, input validation, CSRF, and injection prevention.
1. Helmet — Secure HTTP Headers
const helmet = require('helmet');
app.use(helmet()); // applies all defaults
// Custom CSP for SPAs
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'", 'cdn.jsdelivr.net'],
styleSrc: ["'self'", "'unsafe-inline'", 'fonts.googleapis.com'],
fontSrc: ["'self'", 'fonts.gstatic.com'],
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'", 'api.example.com'],
frameSrc: ["'none'"],
objectSrc: ["'none'"],
upgradeInsecureRequests: [],
},
}));
// Separate header controls
app.use(helmet.hsts({ maxAge: 63072000, includeSubDomains: true, preload: true }));
app.use(helmet.noSniff());
app.use(helmet.frameguard({ action: 'deny' }));
app.use(helmet.xssFilter());
2. Rate Limiting
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const redis = require('ioredis');
const client = new redis(process.env.REDIS_URL);
// General API limiter
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
standardHeaders: true,
legacyHeaders: false,
store: new RedisStore({ sendCommand: (...args) => client.call(...args) }),
message: { error: 'Too many requests, try again later.' },
});
// Strict limiter for auth endpoints
const authLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 10,
skipSuccessfulRequests: true, // only count failures
message: { error: 'Too many failed attempts.' },
});
app.use('/api/', apiLimiter);
app.post('/auth/login', authLimiter, loginHandler);
app.post('/auth/forgot-password', authLimiter, forgotHandler);
3. Input Validation with Zod
const { z } = require('zod');
// Define schemas
const createUserSchema = z.object({
email: z.string().email().max(255),
password: z.string().min(8).max(128).regex(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
'Must contain uppercase, lowercase, and number'
),
name: z.string().min(1).max(100).trim(),
age: z.number().int().min(0).max(150).optional(),
});
// Validation middleware factory
function validate(schema) {
return (req, res, next) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(422).json({
error: 'Validation failed',
fields: result.error.flatten().fieldErrors,
});
}
req.body = result.data; // use parsed (sanitized) data
next();
};
}
app.post('/users', validate(createUserSchema), createUser);
4. CSRF Protection
// Modern approach — Double Submit Cookie pattern
const crypto = require('crypto');
function csrfTokenMiddleware(req, res, next) {
if (!req.cookies['csrf-token']) {
const token = crypto.randomBytes(32).toString('hex');
res.cookie('csrf-token', token, {
httpOnly: false, // JS needs to read it
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
});
}
next();
}
function csrfVerify(req, res, next) {
const safeMethods = ['GET', 'HEAD', 'OPTIONS'];
if (safeMethods.includes(req.method)) return next();
const cookieToken = req.cookies['csrf-token'];
const headerToken = req.headers['x-csrf-token'];
if (!cookieToken || cookieToken !== headerToken) {
return res.status(403).json({ error: 'CSRF token invalid' });
}
next();
}
app.use(cookieParser());
app.use(csrfTokenMiddleware);
app.use(csrfVerify);
5. SQL Injection Prevention
// NEVER: string interpolation in queries
// BAD: db.query(`SELECT * FROM users WHERE id = ${req.params.id}`)
// GOOD: Parameterized queries (pg)
const { Pool } = require('pg');
const pool = new Pool();
app.get('/users/:id', async (req, res) => {
const { rows } = await pool.query(
'SELECT id, name, email FROM users WHERE id = $1',
[req.params.id] // parameter, never interpolated
);
res.json(rows[0] ?? null);
});
// GOOD: ORM (Prisma)
app.get('/users', async (req, res) => {
const users = await prisma.user.findMany({
where: { email: { contains: req.query.search } },
select: { id: true, name: true, email: true }, // never select passwords
});
res.json(users);
});
6. Security Checklist
| Area | Action | Package |
|---|---|---|
| HTTP Headers | Set secure headers | helmet |
| Rate Limiting | Throttle requests | express-rate-limit |
| Input | Validate & sanitize | zod / joi |
| Passwords | Hash with bcrypt | bcryptjs |
| Sessions | Secure cookie flags | express-session |
| CORS | Allowlist origins | cors |
| CSRF | Double-submit cookie | custom / csurf |
| SQL | Parameterized queries | ORM / pg |
| Secrets | Use env vars | dotenv |
| Dependencies | Audit regularly | npm audit |