HEX
Server: Apache/2.4.65 (Debian)
System: Linux kubikelcreative 5.10.0-35-amd64 #1 SMP Debian 5.10.237-1 (2025-05-19) x86_64
User: www-data (33)
PHP: 8.4.13
Disabled: NONE
Upload Files
File: /var/www/indoadvisory_new/web2/webapp/middleware/security.js
const csrf = require('csurf');
const rateLimit = require('express-rate-limit');
const { body, validationResult } = require('express-validator');
const multer = require('multer');
const path = require('path');
const fs = require('fs');

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

// Input Validation
// ================

/**
 * Validate common inputs with enterprise security
 */
const validateEmail = body('email')
  .isEmail()
  .normalizeEmail()
  .withMessage('Please provide a valid email address')
  .isLength({ max: 255 })
  .withMessage('Email must be less than 255 characters');

const validatePassword = body('password')
  .isLength({ min: 8 })
  .withMessage('Password must be at least 8 characters long')
  .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/)
  .withMessage('Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character');

const validateName = body('name')
  .trim()
  .isLength({ min: 2, max: 255 })
  .withMessage('Name must be between 2 and 255 characters')
  .matches(/^[a-zA-Z\s\u00C0-\u017F\u0100-\u024F\u1E00-\u1EFF]+$/)
  .withMessage('Name can only contain letters and spaces');

const validatePhone = body('phone')
  .optional()
  .matches(/^[\+]?[1-9][\d]{0,15}$/)
  .withMessage('Please provide a valid phone number');

const validateURL = (field) => body(field)
  .optional()
  .isURL()
  .withMessage(`${field} must be a valid URL`);

/**
 * Check for validation errors
 */
const checkValidationResult = (req, res, next) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    const errorMessages = errors.array().map(error => error.msg);
    req.flash('error', errorMessages.join(', '));
    return res.redirect('back');
  }
  next();
};

/**
 * API validation error handler
 */
const handleAPIValidation = (req, res, next) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({
      error: 'Validation failed',
      details: errors.array()
    });
  }
  next();
};

// File Upload Security
// ====================

/**
 * Secure file upload configuration
 */
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    const uploadPath = path.join(__dirname, '..', 'uploads');
    
    // Create uploads directory if it doesn't exist
    if (!fs.existsSync(uploadPath)) {
      fs.mkdirSync(uploadPath, { recursive: true });
    }
    
    cb(null, uploadPath);
  },
  filename: (req, file, cb) => {
    // Generate secure filename
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
    const ext = path.extname(file.originalname).toLowerCase();
    const name = file.originalname.replace(/[^a-zA-Z0-9]/g, '_').substring(0, 50);
    cb(null, `${name}_${uniqueSuffix}${ext}`);
  }
});

/**
 * File filter for security
 */
const fileFilter = (req, file, cb) => {
  const allowedTypes = process.env.ALLOWED_FILE_TYPES 
    ? process.env.ALLOWED_FILE_TYPES.split(',') 
    : ['jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx'];
  
  const ext = path.extname(file.originalname).toLowerCase().substring(1);
  
  if (allowedTypes.includes(ext)) {
    cb(null, true);
  } else {
    cb(new Error(`File type .${ext} is not allowed. Allowed types: ${allowedTypes.join(', ')}`), false);
  }
};

const upload = multer({
  storage: storage,
  limits: {
    fileSize: parseInt(process.env.MAX_FILE_SIZE) || 5242880, // 5MB default
    files: 10 // Maximum 10 files per upload
  },
  fileFilter: fileFilter
});

/**
 * Handle multer errors
 */
const handleUploadError = (err, req, res, next) => {
  if (err instanceof multer.MulterError) {
    let message = 'File upload error';
    
    switch (err.code) {
      case 'LIMIT_FILE_SIZE':
        message = 'File too large. Maximum size is 5MB.';
        break;
      case 'LIMIT_FILE_COUNT':
        message = 'Too many files. Maximum 10 files allowed.';
        break;
      case 'LIMIT_UNEXPECTED_FILE':
        message = 'Unexpected file field.';
        break;
    }
    
    req.flash('error', message);
    return res.redirect('back');
  }
  
  if (err) {
    req.flash('error', err.message);
    return res.redirect('back');
  }
  
  next();
};

// Rate Limiting
// =============

/**
 * Enhanced rate limiting for different endpoints
 */
const createRateLimit = (windowMs, max, message) => {
  return rateLimit({
    windowMs,
    max,
    message,
    standardHeaders: true,
    legacyHeaders: false,
    handler: (req, res) => {
      if (req.accepts('html')) {
        req.flash('error', message);
        return res.redirect('back');
      } else {
        return res.status(429).json({ error: message });
      }
    }
  });
};

const loginRateLimit = createRateLimit(
  15 * 60 * 1000, // 15 minutes
  5, // 5 attempts
  'Too many login attempts. Please try again in 15 minutes.'
);

const apiRateLimit = createRateLimit(
  15 * 60 * 1000, // 15 minutes
  100, // 100 requests
  'Too many API requests. Please try again later.'
);

const uploadRateLimit = createRateLimit(
  60 * 1000, // 1 minute
  5, // 5 uploads
  'Too many file uploads. Please wait before uploading again.'
);

// Content Security
// ================

/**
 * Sanitize HTML content to prevent XSS
 */
const sanitizeHtml = (html) => {
  if (!html) return '';
  
  // Basic HTML sanitization - remove script tags and dangerous attributes
  return html
    .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
    .replace(/on\w+\s*=\s*"[^"]*"/g, '')
    .replace(/on\w+\s*=\s*'[^']*'/g, '')
    .replace(/javascript:/gi, '')
    .replace(/vbscript:/gi, '')
    .replace(/data:text\/html/gi, '');
};

/**
 * Validate and sanitize rich text content
 */
const validateRichText = body('content')
  .custom((value) => {
    if (!value || value.length === 0) {
      throw new Error('Content is required');
    }
    
    if (value.length > 50000) {
      throw new Error('Content is too long (maximum 50,000 characters)');
    }
    
    return true;
  })
  .customSanitizer(sanitizeHtml);

// Security Headers
// ================

/**
 * Additional security headers middleware
 */
const securityHeaders = (req, res, next) => {
  // Prevent content type sniffing
  res.setHeader('X-Content-Type-Options', 'nosniff');
  
  // Prevent page from being embedded in frames
  res.setHeader('X-Frame-Options', 'DENY');
  
  // Enable XSS protection
  res.setHeader('X-XSS-Protection', '1; mode=block');
  
  // Referrer policy
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
  
  // Permission policy
  res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
  
  next();
};

/**
 * IP-based access control
 */
const ipWhitelist = (allowedIPs = []) => {
  return (req, res, next) => {
    if (allowedIPs.length === 0) {
      return next(); // No IP restrictions if empty
    }
    
    const clientIP = req.ip || req.connection.remoteAddress;
    
    if (!allowedIPs.includes(clientIP)) {
      console.log(`Blocked access from IP: ${clientIP}`);
      return res.status(403).json({ error: 'Access denied' });
    }
    
    next();
  };
};

module.exports = {
  // CSRF Protection
  csrfProtection,
  
  // Input Validation
  validateEmail,
  validatePassword,
  validateName,
  validatePhone,
  validateURL,
  validateRichText,
  checkValidationResult,
  handleAPIValidation,
  sanitizeHtml,
  
  // File Upload
  upload,
  handleUploadError,
  
  // Rate Limiting
  loginRateLimit,
  apiRateLimit,
  uploadRateLimit,
  createRateLimit,
  
  // Security Headers
  securityHeaders,
  ipWhitelist
};