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:

  1. Injection — SQL/NoSQL injection, command injection
  2. Broken Authentication — Weak passwords, session hijacking
  3. Sensitive Data Exposure — Unencrypted data, exposed secrets
  4. XML/JSON External Entities — Malicious payloads
  5. Broken Access Control — Missing authorization checks
  6. Security Misconfiguration — Default settings, verbose errors
  7. Cross-Site Scripting (XSS) — Unescaped user input in HTML
  8. Insecure Deserialization — Untrusted data in JSON.parse()
  9. Using Components with Known Vulnerabilities — Outdated npm packages
  10. 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 .env files — 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

codeTap to expand ⛶
1// Security Best Practices — Production Hardening
2
3const express = require("express");
4const helmet = require("helmet");
5const cors = require("cors");
6const rateLimit = require("express-rate-limit");
7
8const app = express();
9
10// 1. Helmet — Sets 15+ security headers
11app.use(helmet());
12// Includes: CSP, X-Content-Type-Options, X-Frame-Options, HSTS, etc.
13
14// 2. CORS — Restrict origins
15app.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);
24
25// 3. Rate limiting
26const apiLimiter = rateLimit({
27 windowMs: 15 * 60 * 1000, // 15 minutes
28 max: 100, // 100 requests per window per IP
29 standardHeaders: true,
30 legacyHeaders: false,
31 message: { error: "Too many requests, please try again later" },
32});
33app.use("/api/", apiLimiter);
34
35// Aggressive rate limiting for auth endpoints
36const authLimiter = rateLimit({
37 windowMs: 15 * 60 * 1000,
38 max: 10, // Only 10 login attempts per 15 minutes
39 message: { error: "Too many login attempts" },
40});
41app.use("/api/auth/login", authLimiter);
42
43// 4. Body size limit (prevent DoS)
44app.use(express.json({ limit: "10kb" }));
45
46// 5. NoSQL injection prevention
47function sanitizeInput(req, res, next) {
48 const sanitize = (obj) => {
49 for (const key in obj) {
50 if (typeof obj[key] === "string") {
51 // Remove MongoDB operators
52 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);
64
65// 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]);
70
71// 7. Password hashing with bcrypt
72const bcrypt = require("bcryptjs");
73
74async function hashPassword(plainPassword) {
75 const salt = await bcrypt.genSalt(12); // Cost factor 12
76 return bcrypt.hash(plainPassword, salt);
77}
78
79async function verifyPassword(plainPassword, hashedPassword) {
80 return bcrypt.compare(plainPassword, hashedPassword);
81}
82
83// 8. HTTP-only cookies for tokens
84function setTokenCookie(res, token) {
85 res.cookie("accessToken", token, {
86 httpOnly: true, // Can't be accessed by JavaScript
87 secure: process.env.NODE_ENV === "production", // HTTPS only in production
88 sameSite: "strict", // No cross-site requests
89 maxAge: 15 * 60 * 1000, // 15 minutes
90 path: "/",
91 });
92}
93
94// 9. Input validation with express-validator
95// 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 user
106// }
107// );
108
109// 10. Security headers for API responses
110app.use((req, res, next) => {
111 res.removeHeader("X-Powered-By"); // Don't reveal Express
112 res.setHeader("X-Content-Type-Options", "nosniff");
113 res.setHeader("X-Frame-Options", "DENY");
114 res.setHeader("Cache-Control", "no-store"); // Prevent caching sensitive data
115 next();
116});
117
118// 11. Graceful error handling (never leak stack traces)
119app.use((err, req, res, next) => {
120 console.error("Error:", err); // Log full error internally
121
122 // Never send stack traces to clients in production
123 res.status(err.statusCode || 500).json({
124 error: process.env.NODE_ENV === "production"
125 ? "Internal server error"
126 : err.message,
127 });
128});
129
130app.listen(3000);

🏋️ Practice Exercise

Exercises:

  1. Set up Helmet and configure Content-Security-Policy for your application
  2. Implement CORS with a whitelist of allowed origins from environment variables
  3. Add rate limiting with different limits for public routes vs auth routes
  4. Sanitize all user input against NoSQL injection and XSS attacks
  5. Implement secure cookie-based JWT authentication with httpOnly and secure flags
  6. Run npm audit and 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 audit shows known vulnerabilities; old packages are the #1 attack vector

  • Using eval(), new Function(), or child_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.