Web Security Essentials: Protecting Your Applications

Web security isn’t optional—it’s essential. Let’s explore the critical security measures every developer needs to implement to protect applications and user data from modern threats.

Authentication and Authorization

Secure Password Handling

const bcrypt = require('bcrypt');
const crypto = require('crypto');

class PasswordService {
  static async hashPassword(password) {
    // Validate password strength
    if (!this.isStrongPassword(password)) {
      throw new Error('Password does not meet security requirements');
    }
    
    const saltRounds = 12; // Adjust based on security needs vs performance
    return await bcrypt.hash(password, saltRounds);
  }
  
  static async verifyPassword(password, hashedPassword) {
    return await bcrypt.compare(password, hashedPassword);
  }
  
  static isStrongPassword(password) {
    // At least 8 characters, 1 uppercase, 1 lowercase, 1 number, 1 special char
    const strongPasswordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
    return strongPasswordRegex.test(password);
  }
  
  static generateSecureToken(length = 32) {
    return crypto.randomBytes(length).toString('hex');
  }
}

// Usage in user registration
app.post('/register', async (req, res) => {
  try {
    const { email, password } = req.body;
    
    // Check if user exists
    const existingUser = await User.findOne({ email });
    if (existingUser) {
      return res.status(400).json({ error: 'User already exists' });
    }
    
    // Hash password
    const hashedPassword = await PasswordService.hashPassword(password);
    
    // Create user
    const user = await User.create({
      email,
      password: hashedPassword,
      emailVerificationToken: PasswordService.generateSecureToken()
    });
    
    // Send verification email
    await sendVerificationEmail(user.email, user.emailVerificationToken);
    
    res.status(201).json({ message: 'User created. Please verify your email.' });
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

JWT Security Best Practices

const jwt = require('jsonwebtoken');
const redis = require('redis');
const client = redis.createClient();

class JWTService {
  static generateTokens(payload) {
    const accessToken = jwt.sign(
      payload,
      process.env.JWT_ACCESS_SECRET,
      { 
        expiresIn: '15m',
        issuer: 'your-app',
        audience: 'your-app-users'
      }
    );
    
    const refreshToken = jwt.sign(
      { userId: payload.userId },
      process.env.JWT_REFRESH_SECRET,
      { 
        expiresIn: '7d',
        issuer: 'your-app',
        audience: 'your-app-users'
      }
    );
    
    return { accessToken, refreshToken };
  }
  
  static async verifyAccessToken(token) {
    try {
      // Check if token is blacklisted
      const isBlacklisted = await client.get(`blacklist:${token}`);
      if (isBlacklisted) {
        throw new Error('Token is blacklisted');
      }
      
      return jwt.verify(token, process.env.JWT_ACCESS_SECRET, {
        issuer: 'your-app',
        audience: 'your-app-users'
      });
    } catch (error) {
      throw new Error('Invalid or expired token');
    }
  }
  
  static async blacklistToken(token) {
    const decoded = jwt.decode(token);
    const expiresIn = decoded.exp - Math.floor(Date.now() / 1000);
    
    if (expiresIn > 0) {
      await client.setex(`blacklist:${token}`, expiresIn, 'true');
    }
  }
  
  static async refreshTokens(refreshToken) {
    try {
      const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
      
      // Check if refresh token is still valid in database
      const user = await User.findById(decoded.userId);
      if (!user || user.refreshTokenVersion !== decoded.tokenVersion) {
        throw new Error('Invalid refresh token');
      }
      
      return this.generateTokens({
        userId: user.id,
        email: user.email,
        role: user.role
      });
    } catch (error) {
      throw new Error('Invalid refresh token');
    }
  }
}

// Secure authentication middleware
const authenticateToken = async (req, res, next) => {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];
  
  if (!token) {
    return res.status(401).json({ error: 'Access token required' });
  }
  
  try {
    const decoded = await JWTService.verifyAccessToken(token);
    req.user = decoded;
    next();
  } catch (error) {
    res.status(403).json({ error: error.message });
  }
};

Input Validation and Sanitization

Comprehensive Input Validation

const validator = require('validator');
const DOMPurify = require('isomorphic-dompurify');

class InputValidator {
  static sanitizeString(input, maxLength = 255) {
    if (typeof input !== 'string') {
      throw new Error('Input must be a string');
    }
    
    // Remove null bytes and control characters
    let sanitized = input.replace(/[\x00-\x1F\x7F]/g, '');
    
    // Trim whitespace
    sanitized = sanitized.trim();
    
    // Limit length
    if (sanitized.length > maxLength) {
      throw new Error(`Input exceeds maximum length of ${maxLength}`);
    }
    
    return sanitized;
  }
  
  static sanitizeHTML(input) {
    return DOMPurify.sanitize(input, {
      ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br'],
      ALLOWED_ATTR: []
    });
  }
  
  static validateEmail(email) {
    if (!validator.isEmail(email)) {
      throw new Error('Invalid email format');
    }
    
    // Additional checks
    if (email.length > 254) {
      throw new Error('Email too long');
    }
    
    return validator.normalizeEmail(email);
  }
  
  static validateURL(url) {
    if (!validator.isURL(url, {
      protocols: ['http', 'https'],
      require_protocol: true,
      require_valid_protocol: true
    })) {
      throw new Error('Invalid URL format');
    }
    
    return url;
  }
  
  static validateInteger(value, min = null, max = null) {
    const num = parseInt(value, 10);
    
    if (isNaN(num)) {
      throw new Error('Value must be an integer');
    }
    
    if (min !== null && num < min) {
      throw new Error(`Value must be at least ${min}`);
    }
    
    if (max !== null && num > max) {
      throw new Error(`Value must be at most ${max}`);
    }
    
    return num;
  }
}

// SQL Injection Prevention
const db = require('pg');
const pool = new db.Pool(/* config */);

// BAD: Vulnerable to SQL injection
const getUserBad = async (userId) => {
  const query = `SELECT * FROM users WHERE id = ${userId}`;
  return await pool.query(query);
};

// GOOD: Using parameterized queries
const getUserGood = async (userId) => {
  const query = 'SELECT * FROM users WHERE id = $1';
  return await pool.query(query, [userId]);
};

// NoSQL Injection Prevention (MongoDB)
const getUserMongo = async (userId) => {
  // Validate input type
  if (typeof userId !== 'string') {
    throw new Error('Invalid user ID format');
  }
  
  // Use strict matching
  return await User.findOne({ 
    _id: mongoose.Types.ObjectId(userId) 
  });
};

Cross-Site Scripting (XSS) Prevention

Content Security Policy

const helmet = require('helmet');

app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    styleSrc: [
      "'self'",
      "'unsafe-inline'", // Only if absolutely necessary
      "https://fonts.googleapis.com"
    ],
    scriptSrc: [
      "'self'",
      "https://apis.google.com"
    ],
    imgSrc: [
      "'self'",
      "data:",
      "https:"
    ],
    connectSrc: [
      "'self'",
      "https://api.example.com"
    ],
    fontSrc: [
      "'self'",
      "https://fonts.gstatic.com"
    ],
    objectSrc: ["'none'"],
    mediaSrc: ["'self'"],
    frameSrc: ["'none'"],
    upgradeInsecureRequests: []
  }
}));

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

Output Encoding

const he = require('he');

class OutputEncoder {
  static encodeHTML(input) {
    return he.encode(input, {
      useNamedReferences: true,
      decimal: false
    });
  }
  
  static encodeHTMLAttribute(input) {
    return he.encode(input, {
      useNamedReferences: true,
      decimal: false,
      encodeEverything: true
    });
  }
  
  static encodeURL(input) {
    return encodeURIComponent(input);
  }
  
  static encodeJS(input) {
    return JSON.stringify(input).slice(1, -1); // Remove quotes
  }
}

// Template rendering with encoding
app.get('/profile', authenticateToken, async (req, res) => {
  const user = await User.findById(req.user.userId);
  
  res.render('profile', {
    userName: OutputEncoder.encodeHTML(user.name),
    userBio: OutputEncoder.encodeHTML(user.bio),
    avatarUrl: OutputEncoder.encodeHTMLAttribute(user.avatarUrl)
  });
});

Cross-Site Request Forgery (CSRF) Protection

CSRF Token Implementation

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

// Configure CSRF protection
const csrfProtection = csrf({
  cookie: {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict'
  }
});

app.use(cookieParser());
app.use(csrfProtection);

// Provide CSRF token to frontend
app.get('/csrf-token', (req, res) => {
  res.json({ csrfToken: req.csrfToken() });
});

// Double Submit Cookie Pattern (alternative)
class CSRFService {
  static generateToken() {
    return crypto.randomBytes(32).toString('hex');
  }
  
  static setCSRFCookie(res, token) {
    res.cookie('csrf-token', token, {
      httpOnly: false, // Accessible to JavaScript
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'strict',
      maxAge: 3600000 // 1 hour
    });
  }
  
  static validateCSRF(req) {
    const cookieToken = req.cookies['csrf-token'];
    const headerToken = req.headers['x-csrf-token'];
    
    if (!cookieToken || !headerToken || cookieToken !== headerToken) {
      throw new Error('CSRF token validation failed');
    }
  }
}

Rate Limiting and DDoS Protection

Advanced Rate Limiting

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

const redisClient = redis.createClient();

// Different rate limits for different endpoints
const createRateLimiter = (windowMs, max, message, skipSuccessfulRequests = false) => {
  return rateLimit({
    store: new RedisStore({
      client: redisClient,
      prefix: 'rl:'
    }),
    windowMs,
    max,
    message: { error: message },
    standardHeaders: true,
    legacyHeaders: false,
    skipSuccessfulRequests,
    keyGenerator: (req) => {
      // Use IP + User ID for authenticated requests
      return req.user ? `${req.ip}:${req.user.userId}` : req.ip;
    }
  });
};

// Progressive delay for repeated requests
const speedLimiter = slowDown({
  store: new RedisStore({
    client: redisClient,
    prefix: 'sl:'
  }),
  windowMs: 15 * 60 * 1000, // 15 minutes
  delayAfter: 5, // Allow 5 requests per windowMs without delay
  delayMs: 500, // Add 500ms delay per request after delayAfter
  maxDelayMs: 20000 // Maximum delay of 20 seconds
});

// Apply different limits
app.use('/api/auth/login', createRateLimiter(15 * 60 * 1000, 5, 'Too many login attempts'));
app.use('/api/auth/register', createRateLimiter(60 * 60 * 1000, 3, 'Too many registration attempts'));
app.use('/api/', speedLimiter);
app.use('/api/', createRateLimiter(15 * 60 * 1000, 100, 'Too many requests'));

// IP-based blocking for suspicious activity
class SecurityMonitor {
  constructor() {
    this.suspiciousIPs = new Map();
    this.blockedIPs = new Set();
  }
  
  recordSuspiciousActivity(ip, activity) {
    const current = this.suspiciousIPs.get(ip) || { count: 0, activities: [] };
    current.count++;
    current.activities.push({ activity, timestamp: Date.now() });
    
    // Keep only recent activities (last hour)
    current.activities = current.activities.filter(
      a => Date.now() - a.timestamp < 3600000
    );
    
    this.suspiciousIPs.set(ip, current);
    
    // Block IP if too many suspicious activities
    if (current.count > 10) {
      this.blockedIPs.add(ip);
      console.warn(`Blocked suspicious IP: ${ip}`);
    }
  }
  
  isBlocked(ip) {
    return this.blockedIPs.has(ip);
  }
  
  middleware() {
    return (req, res, next) => {
      if (this.isBlocked(req.ip)) {
        return res.status(403).json({ error: 'Access denied' });
      }
      next();
    };
  }
}

const securityMonitor = new SecurityMonitor();
app.use(securityMonitor.middleware());

Secure Headers and HTTPS

Security Headers Implementation

const helmet = require('helmet');

app.use(helmet({
  // Content Security Policy
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      scriptSrc: ["'self'"],
      imgSrc: ["'self'", "data:", "https:"]
    }
  },
  
  // HTTP Strict Transport Security
  hsts: {
    maxAge: 31536000, // 1 year
    includeSubDomains: true,
    preload: true
  },
  
  // Prevent clickjacking
  frameguard: { action: 'deny' },
  
  // Prevent MIME type sniffing
  noSniff: true,
  
  // XSS Protection
  xssFilter: true,
  
  // Referrer Policy
  referrerPolicy: { policy: 'same-origin' },
  
  // Feature Policy
  featurePolicy: {
    features: {
      camera: ["'none'"],
      microphone: ["'none'"],
      geolocation: ["'self'"]
    }
  }
}));

// HTTPS Redirect
app.use((req, res, next) => {
  if (process.env.NODE_ENV === 'production' && !req.secure && req.get('x-forwarded-proto') !== 'https') {
    return res.redirect(301, `https://${req.get('host')}${req.url}`);
  }
  next();
});

Security Monitoring and Logging

Security Event Logging

const winston = require('winston');

const securityLogger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({ filename: 'security.log' }),
    new winston.transports.Console()
  ]
});

class SecurityLogger {
  static logAuthAttempt(ip, email, success, reason = null) {
    securityLogger.info('Authentication attempt', {
      type: 'auth_attempt',
      ip,
      email,
      success,
      reason,
      timestamp: new Date().toISOString()
    });
  }
  
  static logSuspiciousActivity(ip, activity, details = {}) {
    securityLogger.warn('Suspicious activity detected', {
      type: 'suspicious_activity',
      ip,
      activity,
      details,
      timestamp: new Date().toISOString()
    });
  }
  
  static logSecurityEvent(event, details = {}) {
    securityLogger.error('Security event', {
      type: 'security_event',
      event,
      details,
      timestamp: new Date().toISOString()
    });
  }
}

// Usage in authentication
app.post('/login', async (req, res) => {
  try {
    const { email, password } = req.body;
    const user = await User.findOne({ email });
    
    if (!user || !await PasswordService.verifyPassword(password, user.password)) {
      SecurityLogger.logAuthAttempt(req.ip, email, false, 'Invalid credentials');
      securityMonitor.recordSuspiciousActivity(req.ip, 'failed_login');
      return res.status(401).json({ error: 'Invalid credentials' });
    }
    
    SecurityLogger.logAuthAttempt(req.ip, email, true);
    
    const tokens = JWTService.generateTokens({
      userId: user.id,
      email: user.email,
      role: user.role
    });
    
    res.json(tokens);
  } catch (error) {
    SecurityLogger.logSecurityEvent('login_error', { error: error.message, ip: req.ip });
    res.status(500).json({ error: 'Internal server error' });
  }
});

Web security is an ongoing process, not a one-time implementation. Stay updated with the latest security threats, regularly audit your code, and always follow the principle of least privilege. Remember: security is only as strong as its weakest link.