Lesson 6 of 8
6 min read
Advanced Node.js

Security Best Practices

Protect your Node.js applications from common vulnerabilities

Security is Critical

Node.js applications are often exposed to the internet, making security paramount. This guide covers the most important security practices to protect your applications from common vulnerabilities.

🔐 OWASP Top 10 for Node.js

1. Injection (SQL, NoSQL, Command)
2. Broken Authentication
3. Sensitive Data Exposure
4. XML External Entities (XXE)
5. Broken Access Control
6. Security Misconfiguration
7. Cross-Site Scripting (XSS)
8. Insecure Deserialization
9. Using Components with Vulnerabilities
10. Insufficient Logging

Input Validation

const Joi = require('joi');
const express = require('express');
const app = express();

// Define validation schema
const userSchema = Joi.object({
  username: Joi.string().alphanum().min(3).max(30).required(),
  email: Joi.string().email().required(),
  password: Joi.string().min(8).pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/).required(),
  age: Joi.number().integer().min(13).max(120)
});

// Validation middleware
function validate(schema) {
  return (req, res, next) => {
    const { error, value } = schema.validate(req.body, { 
      abortEarly: false,
      stripUnknown: true  // Remove unknown fields
    });
    
    if (error) {
      return res.status(400).json({
        error: 'Validation failed',
        details: error.details.map(d => d.message)
      });
    }
    
    req.body = value;  // Use sanitized value
    next();
  };
}

app.post('/users', validate(userSchema), (req, res) => {
  // req.body is now validated and sanitized
  createUser(req.body);
});

Preventing Injection Attacks

// SQL Injection Prevention
// BAD: Concatenating user input
const query = `SELECT * FROM users WHERE id = ${userId}`;

// GOOD: Use parameterized queries
const { Pool } = require('pg');
const pool = new Pool();

// PostgreSQL
const result = await pool.query(
  'SELECT * FROM users WHERE id = $1',
  [userId]
);

// MySQL
const mysql = require('mysql2/promise');
const [rows] = await connection.execute(
  'SELECT * FROM users WHERE id = ?',
  [userId]
);

// MongoDB Injection Prevention
// BAD: User can inject operators
const user = await User.findOne({ username: req.body.username });
// If username = { "$gt": "" }, returns first user!

// GOOD: Validate type
const username = String(req.body.username);
const user = await User.findOne({ username });

// Or use mongoose schema validation
const userSchema = new mongoose.Schema({
  username: { type: String, required: true }
});

// Command Injection Prevention
// BAD: Shell command with user input
const { exec } = require('child_process');
exec(`ls ${userInput}`);  // Dangerous!

// GOOD: Use execFile with arguments array
const { execFile } = require('child_process');
execFile('ls', [userInput], (err, stdout) => {
  // Safe: arguments are not shell-interpreted
});

Using Helmet for HTTP Headers

const express = require('express');
const helmet = require('helmet');
const app = express();

// Use all default protections
app.use(helmet());

// Or configure individually
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'unsafe-inline'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:"],
    },
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  },
  referrerPolicy: { policy: 'strict-origin-when-cross-origin' }
}));

// Headers helmet sets:
// X-Content-Type-Options: nosniff
// X-Frame-Options: DENY
// X-XSS-Protection: 0 (deprecated, CSP is better)
// Strict-Transport-Security: max-age=...
// Content-Security-Policy: ...
// And more...

Rate Limiting

const rateLimit = require('express-rate-limit');
const slowDown = require('express-slow-down');
const RedisStore = require('rate-limit-redis');
const { createClient } = require('redis');

// Basic rate limiting
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // 100 requests per window
  message: { error: 'Too many requests, please try again later' },
  standardHeaders: true,
  legacyHeaders: false,
});

app.use('/api/', limiter);

// Stricter limits for auth endpoints
const authLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour
  max: 5, // 5 login attempts per hour
  skipSuccessfulRequests: true,
});

app.use('/api/login', authLimiter);

// Distributed rate limiting with Redis
const redisClient = createClient({ url: 'redis://localhost:6379' });

const distributedLimiter = rateLimit({
  store: new RedisStore({
    sendCommand: (...args) => redisClient.sendCommand(args),
  }),
  windowMs: 15 * 60 * 1000,
  max: 100,
});

// Slow down instead of blocking
const speedLimiter = slowDown({
  windowMs: 15 * 60 * 1000,
  delayAfter: 50, // Allow 50 requests, then slow down
  delayMs: 500, // Add 500ms delay per request after limit
});

app.use(speedLimiter);

HTTPS and Secure Cookies

const https = require('https');
const fs = require('fs');
const express = require('express');

const app = express();

// Force HTTPS in production
if (process.env.NODE_ENV === 'production') {
  app.use((req, res, next) => {
    if (req.header('x-forwarded-proto') !== 'https') {
      res.redirect(`https://${req.header('host')}${req.url}`);
    } else {
      next();
    }
  });
}

// Secure cookie settings
const session = require('express-session');

app.use(session({
  secret: process.env.SESSION_SECRET,
  name: 'sessionId', // Don't use default 'connect.sid'
  cookie: {
    secure: process.env.NODE_ENV === 'production', // HTTPS only
    httpOnly: true,   // No JavaScript access
    sameSite: 'strict', // CSRF protection
    maxAge: 24 * 60 * 60 * 1000 // 24 hours
  },
  resave: false,
  saveUninitialized: false
}));

// HTTPS server
const options = {
  key: fs.readFileSync('private-key.pem'),
  cert: fs.readFileSync('certificate.pem')
};

https.createServer(options, app).listen(443);

Dependency Security

# Check for vulnerabilities
npm audit

# Fix vulnerabilities automatically
npm audit fix

# Force fix (may include breaking changes)
npm audit fix --force

# Use Snyk for deeper analysis
npm install -g snyk
snyk test
snyk monitor  # Continuous monitoring

# In package.json - lock versions
{
  "dependencies": {
    "express": "4.18.2"  // Exact version, not ^4.18.2
  }
}

# Use .npmrc for security
# .npmrc
audit=true
package-lock=true
ignore-scripts=true  # Don't run postinstall scripts

Environment Variables & Secrets

// NEVER commit secrets to git!
// Use .env files (add to .gitignore)

// .env
DATABASE_URL=postgres://user:pass@localhost/db
JWT_SECRET=super-secret-key-change-me
API_KEY=sk-1234567890

// Load with dotenv
require('dotenv').config();

// Access secrets
const dbUrl = process.env.DATABASE_URL;

// Validate required env vars at startup
const required = ['DATABASE_URL', 'JWT_SECRET'];
for (const key of required) {
  if (!process.env[key]) {
    console.error(`Missing required env var: ${key}`);
    process.exit(1);
  }
}

// Use a secrets manager in production
// AWS Secrets Manager, HashiCorp Vault, etc.
const { SecretsManager } = require('@aws-sdk/client-secrets-manager');

async function getSecret(secretName) {
  const client = new SecretsManager();
  const response = await client.getSecretValue({ SecretId: secretName });
  return JSON.parse(response.SecretString);
}

Security Checklist

  • ☐ Use HTTPS everywhere
  • ☐ Validate and sanitize all user input
  • ☐ Use parameterized queries for databases
  • ☐ Implement rate limiting
  • ☐ Set secure HTTP headers with Helmet
  • ☐ Use secure, httpOnly, sameSite cookies
  • ☐ Run npm audit regularly
  • ☐ Keep dependencies updated
  • ☐ Never commit secrets to git
  • ☐ Implement proper logging and monitoring
  • ☐ Use strong password hashing (bcrypt, argon2)
  • ☐ Implement proper error handling (don't leak info)

💡 Best Practices

  • • Defense in depth: multiple security layers
  • • Principle of least privilege: minimal permissions
  • • Fail securely: errors should not expose info
  • • Keep security in mind from the start
  • • Regular security audits and penetration testing

Continue Learning