Oreoluwa
Web Security Fundamentals: Protecting Your Applications
February 22, 2024
10 min read

Web Security Fundamentals: Protecting Your Applications

Security
Web Development
Authentication
OWASP

Web security is not optional in today's threat landscape. Every web application is a potential target, and security must be built in from the ground up. Let's explore the fundamental principles and practical techniques for securing web applications.

OWASP Top 10 Vulnerabilities

1. Injection Attacks

SQL injection remains one of the most dangerous vulnerabilities:

// BAD: Vulnerable to SQL injection
app.post('/login', (req, res) => {
  const { username, password } = req.body;
  const query = `SELECT * FROM users WHERE username = '${username}' AND password = '${password}'`;
  db.query(query, (err, results) => {
    // Vulnerable to: admin' --
    if (results.length > 0) {
      res.json({ success: true });
    }
  });
});

// GOOD: Using parameterized queries
app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  
  try {
    // Input validation
    if (!username || !password) {
      return res.status(400).json({ error: 'Username and password required' });
    }
    
    // Parameterized query
    const query = 'SELECT id, username, password_hash FROM users WHERE username = ?';
    const [rows] = await db.execute(query, [username]);
    
    if (rows.length === 0) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }
    
    const user = rows[0];
    const isValid = await bcrypt.compare(password, user.password_hash);
    
    if (isValid) {
      const token = jwt.sign(
        { userId: user.id, username: user.username },
        process.env.JWT_SECRET,
        { expiresIn: '1h' }
      );
      res.json({ token });
    } else {
      res.status(401).json({ error: 'Invalid credentials' });
    }
  } catch (error) {
    console.error('Login error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

2. Cross-Site Scripting (XSS)

Prevent XSS attacks through proper output encoding:

// Input sanitization middleware
const xss = require('xss');
const DOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');

const window = new JSDOM('').window;
const purify = DOMPurify(window);

function sanitizeInput(req, res, next) {
  if (req.body) {
    for (const key in req.body) {
      if (typeof req.body[key] === 'string') {
        // Basic XSS filtering
        req.body[key] = xss(req.body[key], {
          whiteList: {
            p: [],
            br: [],
            strong: [],
            em: [],
            u: []
          },
          stripIgnoreTag: true,
          stripIgnoreTagBody: ['script']
        });
      }
    }
  }
  next();
}

// Content Security Policy
app.use((req, res, next) => {
  res.setHeader(
    'Content-Security-Policy',
    "default-src 'self'; " +
    "script-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; " +
    "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " +
    "font-src 'self' https://fonts.gstatic.com; " +
    "img-src 'self' data: https:; " +
    "connect-src 'self' https://api.example.com"
  );
  next();
});

// Template rendering with automatic escaping
app.set('view engine', 'ejs');
app.locals.escapeHtml = (unsafe) => {
  return unsafe
    .replace(/&/g, "&")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#039;");
};

3. Cross-Site Request Forgery (CSRF)

Implement CSRF protection:

const csrf = require('csurf');

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

app.use(csrfProtection);

// Make CSRF token available to templates
app.use((req, res, next) => {
  res.locals.csrfToken = req.csrfToken();
  next();
});

// In your forms
// <input type="hidden" name="_csrf" value="<%= csrfToken %>">

// For AJAX requests
app.get('/csrf-token', (req, res) => {
  res.json({ csrfToken: req.csrfToken() });
});

// Double Submit Cookie pattern for SPAs
function generateCSRFToken() {
  return crypto.randomBytes(32).toString('hex');
}

app.use('/api', (req, res, next) => {
  if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS') {
    return next();
  }
  
  const tokenFromHeader = req.headers['x-csrf-token'];
  const tokenFromCookie = req.cookies['csrf-token'];
  
  if (!tokenFromHeader || !tokenFromCookie || tokenFromHeader !== tokenFromCookie) {
    return res.status(403).json({ error: 'CSRF token mismatch' });
  }
  
  next();
});

Authentication and Session Management

Secure Password Handling

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

// Password validation
function validatePassword(password) {
  const result = zxcvbn(password);
  
  if (result.score < 3) {
    return {
      valid: false,
      message: 'Password is too weak',
      suggestions: result.feedback.suggestions
    };
  }
  
  if (password.length < 12) {
    return {
      valid: false,
      message: 'Password must be at least 12 characters long'
    };
  }
  
  return { valid: true };
}

// Secure password hashing
async function hashPassword(password) {
  const saltRounds = 12;
  return await bcrypt.hash(password, saltRounds);
}

// User registration with security checks
app.post('/register', async (req, res) => {
  const { username, email, password } = req.body;
  
  try {
    // Validate input
    if (!username || !email || !password) {
      return res.status(400).json({ error: 'All fields required' });
    }
    
    // Email validation
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(email)) {
      return res.status(400).json({ error: 'Invalid email format' });
    }
    
    // Password strength check
    const passwordValidation = validatePassword(password);
    if (!passwordValidation.valid) {
      return res.status(400).json({ 
        error: passwordValidation.message,
        suggestions: passwordValidation.suggestions
      });
    }
    
    // Check if user already exists
    const existingUser = await User.findOne({ 
      $or: [{ email }, { username }] 
    });
    
    if (existingUser) {
      return res.status(409).json({ error: 'User already exists' });
    }
    
    // Hash password
    const passwordHash = await hashPassword(password);
    
    // Create user
    const user = new User({
      username,
      email,
      passwordHash,
      emailVerified: false,
      createdAt: new Date()
    });
    
    await user.save();
    
    // Send verification email
    await sendVerificationEmail(user);
    
    res.status(201).json({ 
      message: 'User created successfully. Please verify your email.' 
    });
    
  } catch (error) {
    console.error('Registration error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

JWT Token Security

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

// Secure JWT configuration
const JWT_CONFIG = {
  accessTokenExpiry: '15m',
  refreshTokenExpiry: '7d',
  algorithm: 'HS256'
};

// Generate token pair
function generateTokens(payload) {
  const accessToken = jwt.sign(
    payload,
    process.env.JWT_ACCESS_SECRET,
    { 
      expiresIn: JWT_CONFIG.accessTokenExpiry,
      algorithm: JWT_CONFIG.algorithm
    }
  );
  
  const refreshToken = jwt.sign(
    { userId: payload.userId },
    process.env.JWT_REFRESH_SECRET,
    { 
      expiresIn: JWT_CONFIG.refreshTokenExpiry,
      algorithm: JWT_CONFIG.algorithm
    }
  );
  
  return { accessToken, refreshToken };
}

// Store refresh token in Redis
async function storeRefreshToken(userId, refreshToken) {
  const tokenId = crypto.randomBytes(16).toString('hex');
  await client.setex(
    `refresh_token:${userId}:${tokenId}`,
    7 * 24 * 60 * 60, // 7 days in seconds
    refreshToken
  );
  return tokenId;
}

// JWT middleware with blacklist checking
async function authenticateToken(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 {
    // Check if token is blacklisted
    const isBlacklisted = await client.get(`blacklist:${token}`);
    if (isBlacklisted) {
      return res.status(401).json({ error: 'Token has been revoked' });
    }
    
    const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET);
    req.user = decoded;
    next();
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired' });
    }
    return res.status(403).json({ error: 'Invalid token' });
  }
}

// Token refresh endpoint
app.post('/refresh-token', async (req, res) => {
  const { refreshToken } = req.body;
  
  if (!refreshToken) {
    return res.status(401).json({ error: 'Refresh token required' });
  }
  
  try {
    const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
    
    // Check if refresh token exists in Redis
    const storedTokens = await client.keys(`refresh_token:${decoded.userId}:*`);
    let tokenValid = false;
    
    for (const key of storedTokens) {
      const storedToken = await client.get(key);
      if (storedToken === refreshToken) {
        tokenValid = true;
        break;
      }
    }
    
    if (!tokenValid) {
      return res.status(401).json({ error: 'Invalid refresh token' });
    }
    
    // Generate new tokens
    const user = await User.findById(decoded.userId);
    const tokens = generateTokens({
      userId: user.id,
      username: user.username,
      role: user.role
    });
    
    // Store new refresh token
    const tokenId = await storeRefreshToken(user.id, tokens.refreshToken);
    
    res.json({
      accessToken: tokens.accessToken,
      refreshToken: tokens.refreshToken
    });
    
  } catch (error) {
    res.status(401).json({ error: 'Invalid refresh token' });
  }
});

Input Validation and Sanitization

const Joi = require('joi');
const validator = require('validator');

// Comprehensive validation schemas
const userSchema = Joi.object({
  username: Joi.string()
    .alphanum()
    .min(3)
    .max(30)
    .required()
    .messages({
      'string.alphanum': 'Username can only contain letters and numbers',
      'string.min': 'Username must be at least 3 characters long',
      'string.max': 'Username cannot exceed 30 characters'
    }),
  
  email: Joi.string()
    .email({ minDomainSegments: 2 })
    .required()
    .messages({
      'string.email': 'Please provide a valid email address'
    }),
  
  password: Joi.string()
    .min(12)
    .pattern(new RegExp('^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])'))
    .required()
    .messages({
      'string.min': 'Password must be at least 12 characters long',
      'string.pattern.base': 'Password must contain uppercase, lowercase, number, and special character'
    }),
  
  age: Joi.number()
    .integer()
    .min(13)
    .max(120)
    .required(),
  
  website: Joi.string()
    .uri()
    .optional(),
  
  bio: Joi.string()
    .max(500)
    .optional()
});

// Validation middleware
function validateInput(schema) {
  return (req, res, next) => {
    const { error, value } = schema.validate(req.body, {
      abortEarly: false,
      stripUnknown: true
    });
    
    if (error) {
      const errors = error.details.map(detail => ({
        field: detail.path.join('.'),
        message: detail.message
      }));
      
      return res.status(400).json({
        error: 'Validation failed',
        details: errors
      });
    }
    
    req.body = value;
    next();
  };
}

// Advanced sanitization
function sanitizeInput(input, type = 'text') {
  if (typeof input !== 'string') return input;
  
  switch (type) {
    case 'email':
      return validator.normalizeEmail(input.trim().toLowerCase());
    
    case 'url':
      return validator.isURL(input) ? input : null;
    
    case 'html':
      return purify.sanitize(input, {
        ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li'],
        ALLOWED_ATTR: []
      });
    
    case 'filename':
      return input.replace(/[^a-zA-Z0-9._-]/g, '').substring(0, 100);
    
    default:
      return input.trim();
  }
}

// File upload security
const multer = require('multer');
const path = require('path');

const upload = multer({
  dest: 'uploads/',
  limits: {
    fileSize: 5 * 1024 * 1024, // 5MB
    files: 1
  },
  fileFilter: (req, file, cb) => {
    // Check file type
    const allowedTypes = /jpeg|jpg|png|gif|pdf|doc|docx/;
    const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
    const mimetype = allowedTypes.test(file.mimetype);
    
    if (mimetype && extname) {
      return cb(null, true);
    } else {
      cb(new Error('Invalid file type'));
    }
  }
});

// Secure file upload endpoint
app.post('/upload', upload.single('file'), async (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ error: 'No file uploaded' });
    }
    
    // Additional security checks
    const fileBuffer = fs.readFileSync(req.file.path);
    
    // Check for malicious content
    if (fileBuffer.includes('<?php') || fileBuffer.includes('<script>')) {
      fs.unlinkSync(req.file.path);
      return res.status(400).json({ error: 'Malicious content detected' });
    }
    
    // Generate secure filename
    const fileExtension = path.extname(req.file.originalname);
    const secureFilename = `${crypto.randomBytes(16).toString('hex')}${fileExtension}`;
    const finalPath = path.join('uploads', secureFilename);
    
    fs.renameSync(req.file.path, finalPath);
    
    res.json({
      message: 'File uploaded successfully',
      filename: secureFilename
    });
    
  } catch (error) {
    console.error('Upload error:', error);
    res.status(500).json({ error: 'Upload failed' });
  }
});

Rate Limiting and DDoS Protection

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

// Basic rate limiting
const generalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  message: {
    error: 'Too many requests from this IP, please try again later'
  },
  standardHeaders: true,
  legacyHeaders: false,
  // Store in Redis for distributed systems
  store: new RedisStore({
    sendCommand: (...args) => client.sendCommand(args),
  }),
});

// Strict rate limiting for authentication endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  message: {
    error: 'Too many login attempts, please try again later'
  },
  skipSuccessfulRequests: true,
});

// Progressive delay for repeated requests
const speedLimiter = slowDown({
  windowMs: 15 * 60 * 1000,
  delayAfter: 10,
  delayMs: 500,
  maxDelayMs: 20000,
});

// IP-based blocking for malicious behavior
const suspiciousIPs = new Set();

function blockSuspiciousIPs(req, res, next) {
  const ip = req.ip;
  
  if (suspiciousIPs.has(ip)) {
    return res.status(403).json({ error: 'IP blocked due to suspicious activity' });
  }
  
  next();
}

// Monitor for suspicious patterns
function detectSuspiciousActivity(req, res, next) {
  const ip = req.ip;
  const userAgent = req.get('User-Agent');
  
  // Check for common attack patterns
  if (!userAgent || userAgent.length < 10) {
    suspiciousIPs.add(ip);
    return res.status(403).json({ error: 'Suspicious request detected' });
  }
  
  // Check for SQL injection patterns in any parameter
  const allParams = { ...req.query, ...req.body, ...req.params };
  for (const [key, value] of Object.entries(allParams)) {
    if (typeof value === 'string') {
      const sqlInjectionPattern = /(\b(SELECT|INSERT|UPDATE|DELETE|DROP|UNION|ALTER)\b)|(--|\/\*|\*\/|;|'|")/i;
      if (sqlInjectionPattern.test(value)) {
        suspiciousIPs.add(ip);
        return res.status(403).json({ error: 'Malicious input detected' });
      }
    }
  }
  
  next();
}

// Apply security middleware
app.use(generalLimiter);
app.use(speedLimiter);
app.use(blockSuspiciousIPs);
app.use(detectSuspiciousActivity);
app.use('/auth', authLimiter);

HTTPS and Security Headers

const helmet = require('helmet');

// Comprehensive security headers
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
      fontSrc: ["'self'", "https://fonts.gstatic.com"],
      imgSrc: ["'self'", "data:", "https:"],
      scriptSrc: ["'self'"],
      connectSrc: ["'self'", "https://api.example.com"]
    }
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  }
}));

// Additional security headers
app.use((req, res, next) => {
  // Prevent clickjacking
  res.setHeader('X-Frame-Options', 'DENY');
  
  // Prevent MIME sniffing
  res.setHeader('X-Content-Type-Options', 'nosniff');
  
  // XSS protection
  res.setHeader('X-XSS-Protection', '1; mode=block');
  
  // Referrer policy
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
  
  // Permissions policy
  res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
  
  next();
});

// 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();
    }
  });
}

Security Best Practices Checklist

  1. Input Validation: Validate and sanitize all input data
  2. Output Encoding: Encode output to prevent XSS
  3. Authentication: Implement strong authentication mechanisms
  4. Authorization: Use proper access controls
  5. HTTPS: Encrypt all data in transit
  6. Secure Headers: Implement comprehensive security headers
  7. Rate Limiting: Protect against brute force attacks
  8. Error Handling: Don't expose sensitive information in errors
  9. Logging: Log security events for monitoring
  10. Dependencies: Keep dependencies updated and secure
  11. Secrets Management: Never hardcode secrets
  12. Regular Testing: Perform security audits and penetration testing

Conclusion

Web security is an ongoing process, not a one-time implementation. Stay informed about the latest threats, regularly update your dependencies, and always follow the principle of defense in depth. Security should be considered at every stage of development, from design to deployment and maintenance.

Remember: it's better to be overly cautious with security than to deal with the consequences of a breach.