⚡ TECH BLOG
Home
Blog
Tags
About
⚡

Powered by Next.js 15 & Modern Web Tech ⚡

Back to Home

Node.js Security Best Practices: Protecting Your Applications

December 15, 2021
nodejssecuritybackendjavascript
Node.js Security Best Practices: Protecting Your Applications

Node.js Security Best Practices: Protecting Your Applications

Security should be a top priority for every Node.js application. This guide covers essential security practices to protect your applications.

Input Validation

Using Joi

const Joi = require('joi');

const userSchema = Joi.object({
  name: Joi.string().min(2).max(50).required(),
  email: Joi.string().email().required(),
  age: Joi.number().integer().min(0).max(120),
  password: Joi.string().min(8).pattern(/^(?=.*[A-Z])(?=.*[0-9])/),
});

app.post('/users', (req, res) => {
  const { error, value } = userSchema.validate(req.body);
  
  if (error) {
    return res.status(400).json({ error: error.details[0].message });
  }
  
  // Use validated value
  createUser(value);
});

Using express-validator

const { body, validationResult } = require('express-validator');

app.post('/users',
  body('email').isEmail().normalizeEmail(),
  body('password').isLength({ min: 8 }),
  body('name').trim().escape(),
  (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    // Process request
  }
);

SQL Injection Prevention

Parameterized Queries

// Bad - SQL Injection vulnerability
app.get('/users', (req, res) => {
  const query = `SELECT * FROM users WHERE id = ${req.query.id}`;
  db.query(query);
});

// Good - Parameterized query
app.get('/users', async (req, res) => {
  const result = await db.query(
    'SELECT * FROM users WHERE id = $1',
    [req.query.id]
  );
  res.json(result.rows);
});

// Using ORM (Prisma)
const user = await prisma.user.findUnique({
  where: { id: parseInt(req.query.id) }
});

XSS Prevention

Helmet.js

const helmet = require('helmet');

app.use(helmet());

// Or configure individual headers
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", "trusted-cdn.com"],
    styleSrc: ["'self'", "'unsafe-inline'"],
    imgSrc: ["'self'", "data:", "trusted-cdn.com"],
  },
}));

app.use(helmet.xssFilter());
app.use(helmet.frameguard({ action: 'deny' }));
app.use(helmet.noSniff());

Sanitizing Output

const sanitizeHtml = require('sanitize-html');

app.post('/comments', (req, res) => {
  const cleanComment = sanitizeHtml(req.body.comment, {
    allowedTags: ['b', 'i', 'em', 'strong'],
    allowedAttributes: {},
  });
  
  saveComment(cleanComment);
});

CSRF Protection

const csrf = require('csurf');
const cookieParser = require('cookie-parser');

app.use(cookieParser());
const csrfProtection = csrf({ cookie: true });

app.get('/form', csrfProtection, (req, res) => {
  res.json({ csrfToken: req.csrfToken() });
});

app.post('/submit', csrfProtection, (req, res) => {
  // Process form
});

Rate Limiting

const rateLimit = require('express-rate-limit');

// General limiter
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // 100 requests per window
  message: 'Too many requests, please try again later.',
});

app.use(limiter);

// Strict limiter for auth routes
const authLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour
  max: 5, // 5 attempts per hour
  skipSuccessfulRequests: true,
});

app.post('/login', authLimiter, loginHandler);

Secure Headers

app.use((req, res, next) => {
  // Prevent clickjacking
  res.setHeader('X-Frame-Options', 'DENY');
  
  // XSS protection
  res.setHeader('X-XSS-Protection', '1; mode=block');
  
  // Prevent MIME type sniffing
  res.setHeader('X-Content-Type-Options', 'nosniff');
  
  // HSTS
  res.setHeader('Strict-Transport-Security', 
    'max-age=31536000; includeSubDomains');
  
  // Disable caching for sensitive routes
  res.setHeader('Cache-Control', 'no-store');
  
  next();
});

Authentication Best Practices

Password Hashing

const bcrypt = require('bcrypt');

// Hashing
async function hashPassword(password) {
  const salt = await bcrypt.genSalt(12);
  return bcrypt.hash(password, salt);
}

// Verification
async function verifyPassword(password, hash) {
  return bcrypt.compare(password, hash);
}

// Registration
app.post('/register', async (req, res) => {
  const hashedPassword = await hashPassword(req.body.password);
  await createUser({
    email: req.body.email,
    password: hashedPassword,
  });
});

JWT Security

const jwt = require('jsonwebtoken');

// Generate token
function generateToken(user) {
  return jwt.sign(
    { userId: user.id, role: user.role },
    process.env.JWT_SECRET,
    { 
      expiresIn: '1h',
      issuer: 'your-app',
      audience: 'your-app-users',
    }
  );
}

// Middleware
function authenticate(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  
  if (!token) {
    return res.status(401).json({ error: 'No token provided' });
  }
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (error) {
    return res.status(401).json({ error: 'Invalid token' });
  }
}

Refresh Token Pattern

const refreshTokens = new Map(); // Use Redis in production

app.post('/refresh', (req, res) => {
  const refreshToken = req.body.refreshToken;
  
  if (!refreshTokens.has(refreshToken)) {
    return res.status(403).json({ error: 'Invalid refresh token' });
  }
  
  const user = refreshTokens.get(refreshToken);
  const newToken = generateToken(user);
  
  res.json({ token: newToken });
});

app.post('/logout', (req, res) => {
  refreshTokens.delete(req.body.refreshToken);
  res.json({ message: 'Logged out' });
});

Environment Variables

// .env file (never commit to git)
DATABASE_URL=postgresql://user:pass@localhost:5432/db
JWT_SECRET=your-super-secret-key
API_KEY=your-api-key

// config.js
require('dotenv').config();

module.exports = {
  database: {
    url: process.env.DATABASE_URL,
  },
  jwt: {
    secret: process.env.JWT_SECRET,
    expiresIn: '1h',
  },
};

// Validate required env vars
const requiredEnvVars = ['DATABASE_URL', 'JWT_SECRET'];
requiredEnvVars.forEach((envVar) => {
  if (!process.env[envVar]) {
    throw new Error(`Missing required env var: ${envVar}`);
  }
});

Secure Dependencies

# Audit dependencies
npm audit

# Fix vulnerabilities
npm audit fix

# Check for outdated packages
npm outdated

# Use npm-force-resolutions for stubborn issues
// package.json
{
  "scripts": {
    "audit": "npm audit --audit-level=moderate",
    "preinstall": "npm audit --audit-level=high || exit 0"
  }
}

Error Handling

// Custom error class
class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = true;
  }
}

// Error handler middleware
app.use((err, req, res, next) => {
  // Log error (don't expose to client)
  console.error(err);
  
  // Don't leak error details in production
  const message = process.env.NODE_ENV === 'production'
    ? 'Something went wrong'
    : err.message;
  
  res.status(err.statusCode || 500).json({
    error: message,
    // Never send stack traces to client
  });
});

Logging and Monitoring

const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' }),
  ],
});

// Log security events
app.post('/login', async (req, res) => {
  const user = await authenticate(req.body);
  
  if (!user) {
    logger.warn('Failed login attempt', {
      email: req.body.email,
      ip: req.ip,
      userAgent: req.headers['user-agent'],
    });
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  
  logger.info('User logged in', { userId: user.id });
  res.json({ token: generateToken(user) });
});

Security Checklist

  • Validate and sanitize all input
  • Use parameterized queries
  • Implement rate limiting
  • Use HTTPS everywhere
  • Set secure HTTP headers
  • Hash passwords with bcrypt
  • Implement proper session management
  • Keep dependencies updated
  • Log security events
  • Implement CORS properly
  • Use environment variables for secrets
  • Implement proper error handling

Conclusion

Security is an ongoing process, not a one-time task. Regularly audit your code, keep dependencies updated, and stay informed about new vulnerabilities. Implement these practices from the start of your project.

Share:

💬 Comments