Introduction
Node.js and Express power a significant portion of modern web applications, microservices, and REST APIs. Yet the ecosystem is frequently targeted by HTTP header fingerprinting, lack of input validation, denial-of-service (DoS) attacks, and NoSQL/SQL injection vulnerabilities.
Here are the 5 Golden Rules for securing a Node.js API in production, illustrated with concrete vulnerable code and full remediation examples.
Rule #1 — Secure HTTP Headers with Helmet
By default, an Express server exposes X-Powered-By: Express, immediately revealing the technology stack to automated vulnerability scanners.
❌ Vulnerable Default Express Server
const express = require('express');
const app = express();
// ❌ Exposes X-Powered-By: Express, no security headers applied
app.get('/api/data', (req, res) => {
res.json({ status: "ok" });
});
app.listen(3000);
✅ Secured with Helmet
const express = require('express');
const helmet = require('helmet');
const app = express();
// ✅ Helmet removes X-Powered-By and injects 15 HTTP security headers:
// Strict-Transport-Security (HSTS), X-Frame-Options (Clickjacking),
// X-Content-Type-Options, Content-Security-Policy, Referrer-Policy...
app.use(helmet());
app.get('/api/data', (req, res) => {
res.json({ status: "ok" });
});
app.listen(3000);
Rule #2 — Strict Input Validation with Zod (Anti-Injection & Sanitization)
Never trust client-supplied data. req.body, req.query, and req.params must be explicitly validated and type-checked before processing.
❌ Vulnerable Code (NoSQL Injection / Arbitrary Payload)
// If a user sends { "email": { "$ne": null }, "password": { "$ne": null } }
// a Mongoose query will return the first user from the database without a valid password!
app.post('/api/login', async (req, res) => {
const user = await db.user.findOne({
email: req.body.email,
password: req.body.password
});
res.json(user);
});
✅ Secured with Zod Validation
import { z } from 'zod';
import express from 'express';
const app = express();
app.use(express.json());
const LoginSchema = z.object({
email: z.string().email("Invalid email format"),
password: z.string().min(8, "Password must be at least 8 characters").max(100)
});
app.post('/api/login', async (req, res) => {
const result = LoginSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: "Invalid input data",
details: result.error.format()
});
}
// Process only validated, sanitized data
const { email, password } = result.data;
const user = await authenticateUser(email, password);
return res.json(user);
});
Rule #3 — Rate Limiting Against Brute-Force & DoS Attacks
Without request throttling, your authentication or email endpoints can be flooded by automated bots.
const rateLimit = require('express-rate-limit');
// Strict limiter for authentication endpoints
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // Max 5 attempts per IP
standardHeaders: true,
legacyHeaders: false,
message: { error: "Too many login attempts. Please try again in 15 minutes." }
});
// General API limiter
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100
});
app.use('/api/auth/login', loginLimiter);
app.use('/api/', apiLimiter);
Rule #4 — CORS Configuration (Cross-Origin Resource Sharing)
❌ Dangerous CORS Configuration
// ❌ CRITICAL: Allows ANY external website to make authenticated requests with cookies!
app.use(cors({ origin: '*', credentials: true }));
✅ Secure CORS Configuration
const cors = require('cors');
const allowedOrigins = ['https://bmj-tech.com', 'https://admin.bmj-tech.com'];
app.use(cors({
origin: function (origin, callback) {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Blocked by CORS policy'));
}
},
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true
}));
Rule #5 — Secure Session & JWT Token Storage
// ✅ Always store JWT tokens in HTTP-Only, SameSite=Strict cookies (NEVER in localStorage)
res.cookie('token', jwtToken, {
httpOnly: true, // Prevents XSS token theft via JavaScript
secure: process.env.NODE_ENV === 'production', // Forces HTTPS
sameSite: 'strict', // Prevents CSRF attacks
maxAge: 3600000 // Expires after 1 hour
});
Security Checklist for Node.js / Express
- [ ] Implement Helmet on all Express applications.
- [ ] Validate 100% of incoming requests with Zod.
- [ ] Configure Rate Limiting on sensitive routes (Login, Reset Password, Payments).
- [ ] Restrict CORS to known trusted origin domains only.
- [ ] Store access tokens in HTTP-Only, Secure, SameSite=Strict cookies.