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
};