Secure Node.js Development

Best practices for building secure Node.js applications with Express, Fastify, and other popular frameworks.

Overview

Node.js applications face unique security challenges. This guide covers the most critical security practices to protect your application.

Security Checklist

  • Use parameterized queries for all database operations
  • Implement proper input validation with allowlists
  • Use bcrypt or argon2 for password hashing
  • Set secure HTTP headers with helmet.js
  • Enable CSRF protection for state-changing operations
  • Use rate limiting on authentication endpoints
  • Keep dependencies updated and audit regularly
  • Never expose sensitive data in error messages

Input Validation

Always validate and sanitize user input. Use schema validation libraries like Zod, Joi, or express-validator.

TypeScript
1import { z } from 'zod';
2
3// Define strict schemas
4const userSchema = z.object({
5 email: z.string().email(),
6 password: z.string().min(12).max(128),
7 age: z.number().int().positive().max(150),
8});
9
10// Validate in route handler
11app.post('/register', async (req, res) => {
12 const result = userSchema.safeParse(req.body);
13 if (!result.success) {
14 return res.status(400).json({
15 error: 'Invalid input',
16 details: result.error.issues
17 });
18 }
19 // Proceed with validated data
20 const { email, password, age } = result.data;
21});

Authentication

Use secure password hashing and proper session management.

TypeScript
1import bcrypt from 'bcrypt';
2import jwt from 'jsonwebtoken';
3
4const SALT_ROUNDS = 12;
5
6// Hash passwords with bcrypt
7async function hashPassword(password: string): Promise<string> {
8 return bcrypt.hash(password, SALT_ROUNDS);
9}
10
11// Verify with constant-time comparison
12async function verifyPassword(
13 password: string,
14 hash: string
15): Promise<boolean> {
16 return bcrypt.compare(password, hash);
17}
18
19// JWT with proper configuration
20function generateToken(userId: string): string {
21 return jwt.sign(
22 { sub: userId },
23 process.env.JWT_SECRET!,
24 {
25 algorithm: 'HS256',
26 expiresIn: '1h',
27 issuer: 'myapp'
28 }
29 );
30}

Never Use Weak Hashing

Never use MD5, SHA1, or SHA256 alone for passwords. These are too fast and vulnerable to brute force attacks.

Database Security

Always use parameterized queries to prevent injection attacks.

TypeScript
1// PostgreSQL with parameterized queries
2const result = await pool.query(
3 'SELECT * FROM users WHERE id = $1',
4 [userId]
5);
6
7// MongoDB with sanitization
8const user = await User.findOne({
9 email: String(req.body.email).toLowerCase()
10});
11
12// Prisma ORM (safe by default)
13const user = await prisma.user.findUnique({
14 where: { id: userId }
15});

Dependency Security

Keep dependencies updated and audit for vulnerabilities regularly.

Bash
1# Audit dependencies
2npm audit
3
4# Fix automatically where possible
5npm audit fix
6
7# Use Bloodhound for deep dependency analysis
8bloodhound scan --mode dependencies
9
10# Lock file integrity
11npm ci # Use in CI/CD instead of npm install

Security Headers

Use helmet.js to set secure HTTP headers automatically.

TypeScript
1import helmet from 'helmet';
2import express from 'express';
3
4const app = express();
5
6// Apply security headers
7app.use(helmet({
8 contentSecurityPolicy: {
9 directives: {
10 defaultSrc: ["'self'"],
11 scriptSrc: ["'self'"],
12 styleSrc: ["'self'", "'unsafe-inline'"],
13 imgSrc: ["'self'", "data:", "https:"],
14 },
15 },
16 hsts: {
17 maxAge: 31536000,
18 includeSubDomains: true,
19 preload: true,
20 },
21}));
22
23// Rate limiting
24import rateLimit from 'express-rate-limit';
25
26app.use('/api/auth', rateLimit({
27 windowMs: 15 * 60 * 1000, // 15 minutes
28 max: 5, // 5 attempts
29 message: 'Too many attempts, try again later',
30}));

Run Bloodhound Regularly

Integrate Bloodhound into your CI/CD pipeline to catch vulnerabilities before they reach production.