Security Best Practices
📖 Concept
Security is not a feature — it's a requirement. Node.js applications face specific security threats that must be addressed at every layer.
OWASP Top 10 relevant to Node.js:
- Injection — SQL/NoSQL injection, command injection
- Broken Authentication — Weak passwords, session hijacking
- Sensitive Data Exposure — Unencrypted data, exposed secrets
- XML/JSON External Entities — Malicious payloads
- Broken Access Control — Missing authorization checks
- Security Misconfiguration — Default settings, verbose errors
- Cross-Site Scripting (XSS) — Unescaped user input in HTML
- Insecure Deserialization — Untrusted data in
JSON.parse() - Using Components with Known Vulnerabilities — Outdated npm packages
- Insufficient Logging & Monitoring — Can't detect breaches
Essential security packages:
| Package | Purpose |
|---|---|
helmet |
Sets security HTTP headers (CSP, HSTS, etc.) |
cors |
Configures Cross-Origin Resource Sharing |
express-rate-limit |
Rate limiting per IP |
hpp |
Prevents HTTP Parameter Pollution |
express-mongo-sanitize |
Prevents NoSQL injection |
xss-clean |
Sanitizes user input against XSS |
bcryptjs |
Password hashing |
Environment variable security:
- Never commit
.envfiles — add to.gitignore - Use different secrets per environment
- Rotate secrets regularly
- Use a secrets manager (AWS Secrets Manager, Vault) in production
🏠 Real-world analogy: Securing a Node.js app is like securing a building. You need locks (authentication), key cards per floor (authorization), security cameras (logging), fire doors (input validation), and regular inspections (vulnerability scanning). One weak point compromises everything.
💻 Code Example
1// Security Best Practices — Production Hardening23const express = require("express");4const helmet = require("helmet");5const cors = require("cors");6const rateLimit = require("express-rate-limit");78const app = express();910// 1. Helmet — Sets 15+ security headers11app.use(helmet());12// Includes: CSP, X-Content-Type-Options, X-Frame-Options, HSTS, etc.1314// 2. CORS — Restrict origins15app.use(16 cors({17 origin: process.env.ALLOWED_ORIGINS?.split(",") || "http://localhost:3000",18 methods: ["GET", "POST", "PUT", "DELETE"],19 allowedHeaders: ["Content-Type", "Authorization"],20 credentials: true,21 maxAge: 86400, // Preflight cache (24 hours)22 })23);2425// 3. Rate limiting26const apiLimiter = rateLimit({27 windowMs: 15 * 60 * 1000, // 15 minutes28 max: 100, // 100 requests per window per IP29 standardHeaders: true,30 legacyHeaders: false,31 message: { error: "Too many requests, please try again later" },32});33app.use("/api/", apiLimiter);3435// Aggressive rate limiting for auth endpoints36const authLimiter = rateLimit({37 windowMs: 15 * 60 * 1000,38 max: 10, // Only 10 login attempts per 15 minutes39 message: { error: "Too many login attempts" },40});41app.use("/api/auth/login", authLimiter);4243// 4. Body size limit (prevent DoS)44app.use(express.json({ limit: "10kb" }));4546// 5. NoSQL injection prevention47function sanitizeInput(req, res, next) {48 const sanitize = (obj) => {49 for (const key in obj) {50 if (typeof obj[key] === "string") {51 // Remove MongoDB operators52 obj[key] = obj[key].replace(/\$|\./g, "");53 } else if (typeof obj[key] === "object" && obj[key] !== null) {54 sanitize(obj[key]);55 }56 }57 };58 if (req.body) sanitize(req.body);59 if (req.query) sanitize(req.query);60 if (req.params) sanitize(req.params);61 next();62}63app.use(sanitizeInput);6465// 6. SQL injection prevention (use parameterized queries)66// ❌ VULNERABLE:67// db.query(`SELECT * FROM users WHERE email = '${req.body.email}'`);68// ✅ SAFE:69// db.query("SELECT * FROM users WHERE email = $1", [req.body.email]);7071// 7. Password hashing with bcrypt72const bcrypt = require("bcryptjs");7374async function hashPassword(plainPassword) {75 const salt = await bcrypt.genSalt(12); // Cost factor 1276 return bcrypt.hash(plainPassword, salt);77}7879async function verifyPassword(plainPassword, hashedPassword) {80 return bcrypt.compare(plainPassword, hashedPassword);81}8283// 8. HTTP-only cookies for tokens84function setTokenCookie(res, token) {85 res.cookie("accessToken", token, {86 httpOnly: true, // Can't be accessed by JavaScript87 secure: process.env.NODE_ENV === "production", // HTTPS only in production88 sameSite: "strict", // No cross-site requests89 maxAge: 15 * 60 * 1000, // 15 minutes90 path: "/",91 });92}9394// 9. Input validation with express-validator95// const { body, validationResult } = require("express-validator");96// app.post("/api/users",97// body("email").isEmail().normalizeEmail(),98// body("name").trim().isLength({ min: 2, max: 50 }),99// body("password").isStrongPassword(),100// (req, res) => {101// const errors = validationResult(req);102// if (!errors.isEmpty()) {103// return res.status(400).json({ errors: errors.array() });104// }105// // ... create user106// }107// );108109// 10. Security headers for API responses110app.use((req, res, next) => {111 res.removeHeader("X-Powered-By"); // Don't reveal Express112 res.setHeader("X-Content-Type-Options", "nosniff");113 res.setHeader("X-Frame-Options", "DENY");114 res.setHeader("Cache-Control", "no-store"); // Prevent caching sensitive data115 next();116});117118// 11. Graceful error handling (never leak stack traces)119app.use((err, req, res, next) => {120 console.error("Error:", err); // Log full error internally121122 // Never send stack traces to clients in production123 res.status(err.statusCode || 500).json({124 error: process.env.NODE_ENV === "production"125 ? "Internal server error"126 : err.message,127 });128});129130app.listen(3000);
🏋️ Practice Exercise
Exercises:
- Set up Helmet and configure Content-Security-Policy for your application
- Implement CORS with a whitelist of allowed origins from environment variables
- Add rate limiting with different limits for public routes vs auth routes
- Sanitize all user input against NoSQL injection and XSS attacks
- Implement secure cookie-based JWT authentication with httpOnly and secure flags
- Run
npm auditand fix all vulnerabilities — then set up automated vulnerability scanning in CI
⚠️ Common Mistakes
Trusting client-side validation — always validate on the server; client validation is for UX, server validation is for security
Storing passwords in plain text or with weak hashing (MD5, SHA-1) — use bcrypt with cost factor 10-12
Exposing detailed error messages in production — stack traces and internal errors help attackers; log internally, show generic messages to clients
Not keeping dependencies updated —
npm auditshows known vulnerabilities; old packages are the #1 attack vectorUsing
eval(),new Function(), orchild_process.exec()with user input — these enable code injection attacks
💼 Interview Questions
🎤 Mock Interview
Mock interview is powered by AI for Security Best Practices. Login to unlock this feature.