add changes
This commit is contained in:
262
middleware/sanitization.js
Normal file
262
middleware/sanitization.js
Normal file
@@ -0,0 +1,262 @@
|
||||
const mongoSanitize = require('express-mongo-sanitize');
|
||||
const xss = require('xss-clean');
|
||||
const hpp = require('hpp');
|
||||
const { body, param, query, validationResult } = require('express-validator');
|
||||
const { BadRequestError } = require('../utils/AppError');
|
||||
|
||||
/**
|
||||
* MongoDB NoSQL Injection Prevention
|
||||
* Removes $ and . characters from user input to prevent NoSQL injection
|
||||
*/
|
||||
const sanitizeMongoData = mongoSanitize({
|
||||
replaceWith: '_',
|
||||
onSanitize: ({ req, key }) => {
|
||||
const logger = require('../config/logger');
|
||||
logger.logSecurityEvent(`MongoDB injection attempt detected in ${key}`, req);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* XSS (Cross-Site Scripting) Prevention
|
||||
* Sanitizes user input to prevent XSS attacks
|
||||
*/
|
||||
const sanitizeXSS = xss();
|
||||
|
||||
/**
|
||||
* HTTP Parameter Pollution Prevention
|
||||
* Protects against attacks where parameters are sent multiple times
|
||||
*/
|
||||
const preventHPP = hpp({
|
||||
whitelist: [
|
||||
// Query parameters that are allowed to have multiple values
|
||||
'category',
|
||||
'categoryId',
|
||||
'difficulty',
|
||||
'tags',
|
||||
'keywords',
|
||||
'sort',
|
||||
'fields'
|
||||
]
|
||||
});
|
||||
|
||||
/**
|
||||
* Sanitize request body, query, and params
|
||||
* Custom middleware that runs after mongoSanitize and xss
|
||||
*/
|
||||
const sanitizeInput = (req, res, next) => {
|
||||
// Additional sanitization for specific patterns
|
||||
const sanitizeObject = (obj) => {
|
||||
if (!obj || typeof obj !== 'object') return obj;
|
||||
|
||||
for (const key in obj) {
|
||||
if (typeof obj[key] === 'string') {
|
||||
// Remove null bytes
|
||||
obj[key] = obj[key].replace(/\0/g, '');
|
||||
|
||||
// Trim whitespace
|
||||
obj[key] = obj[key].trim();
|
||||
|
||||
// Remove any remaining dangerous patterns
|
||||
obj[key] = obj[key]
|
||||
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
||||
.replace(/javascript:/gi, '')
|
||||
.replace(/on\w+\s*=/gi, '');
|
||||
} else if (typeof obj[key] === 'object') {
|
||||
sanitizeObject(obj[key]);
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
if (req.body) sanitizeObject(req.body);
|
||||
if (req.query) sanitizeObject(req.query);
|
||||
if (req.params) sanitizeObject(req.params);
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate and sanitize email addresses
|
||||
*/
|
||||
const sanitizeEmail = [
|
||||
body('email')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.isEmail().withMessage('Invalid email format')
|
||||
.normalizeEmail({
|
||||
gmail_remove_dots: false,
|
||||
gmail_remove_subaddress: false,
|
||||
outlookdotcom_remove_subaddress: false,
|
||||
yahoo_remove_subaddress: false,
|
||||
icloud_remove_subaddress: false
|
||||
})
|
||||
.isLength({ max: 255 }).withMessage('Email too long')
|
||||
];
|
||||
|
||||
/**
|
||||
* Validate and sanitize passwords
|
||||
*/
|
||||
const sanitizePassword = [
|
||||
body('password')
|
||||
.trim()
|
||||
.isLength({ min: 8 }).withMessage('Password must be at least 8 characters')
|
||||
.isLength({ max: 128 }).withMessage('Password too long')
|
||||
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/)
|
||||
.withMessage('Password must contain uppercase, lowercase, number and special character')
|
||||
];
|
||||
|
||||
/**
|
||||
* Validate and sanitize usernames
|
||||
*/
|
||||
const sanitizeUsername = [
|
||||
body('username')
|
||||
.trim()
|
||||
.isLength({ min: 3, max: 30 }).withMessage('Username must be 3-30 characters')
|
||||
.matches(/^[a-zA-Z0-9_-]+$/).withMessage('Username can only contain letters, numbers, underscores and hyphens')
|
||||
];
|
||||
|
||||
/**
|
||||
* Validate and sanitize numeric IDs
|
||||
*/
|
||||
const sanitizeId = [
|
||||
param('id')
|
||||
.isInt({ min: 1 }).withMessage('Invalid ID')
|
||||
.toInt()
|
||||
];
|
||||
|
||||
/**
|
||||
* Validate and sanitize pagination parameters
|
||||
*/
|
||||
const sanitizePagination = [
|
||||
query('page')
|
||||
.optional()
|
||||
.isInt({ min: 1, max: 10000 }).withMessage('Invalid page number')
|
||||
.toInt(),
|
||||
query('limit')
|
||||
.optional()
|
||||
.isInt({ min: 1, max: 100 }).withMessage('Limit must be between 1 and 100')
|
||||
.toInt(),
|
||||
query('sort')
|
||||
.optional()
|
||||
.trim()
|
||||
.isIn(['asc', 'desc', 'ASC', 'DESC']).withMessage('Sort must be asc or desc')
|
||||
];
|
||||
|
||||
/**
|
||||
* Validate and sanitize search queries
|
||||
*/
|
||||
const sanitizeSearch = [
|
||||
query('search')
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ max: 200 }).withMessage('Search query too long')
|
||||
.matches(/^[a-zA-Z0-9\s-_.,!?'"]+$/).withMessage('Search contains invalid characters')
|
||||
];
|
||||
|
||||
/**
|
||||
* Validate and sanitize quiz parameters
|
||||
*/
|
||||
const sanitizeQuizParams = [
|
||||
body('categoryId')
|
||||
.isInt({ min: 1 }).withMessage('Invalid category ID')
|
||||
.toInt(),
|
||||
body('questionCount')
|
||||
.optional()
|
||||
.isInt({ min: 1, max: 50 }).withMessage('Question count must be between 1 and 50')
|
||||
.toInt(),
|
||||
body('difficulty')
|
||||
.optional()
|
||||
.trim()
|
||||
.isIn(['easy', 'medium', 'hard']).withMessage('Invalid difficulty level'),
|
||||
body('quizType')
|
||||
.optional()
|
||||
.trim()
|
||||
.isIn(['practice', 'timed', 'exam']).withMessage('Invalid quiz type')
|
||||
];
|
||||
|
||||
/**
|
||||
* Middleware to check validation results
|
||||
*/
|
||||
const handleValidationErrors = (req, res, next) => {
|
||||
const errors = validationResult(req);
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
const logger = require('../config/logger');
|
||||
logger.logSecurityEvent('Validation error', req);
|
||||
|
||||
throw new BadRequestError('Validation failed', errors.array().map(err => ({
|
||||
field: err.path || err.param,
|
||||
message: err.msg
|
||||
})));
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
/**
|
||||
* Comprehensive sanitization middleware chain
|
||||
* Use this for all API routes
|
||||
*/
|
||||
const sanitizeAll = [
|
||||
sanitizeMongoData,
|
||||
sanitizeXSS,
|
||||
preventHPP,
|
||||
sanitizeInput
|
||||
];
|
||||
|
||||
/**
|
||||
* File upload sanitization
|
||||
*/
|
||||
const sanitizeFileUpload = (req, res, next) => {
|
||||
if (req.file) {
|
||||
// Validate file type
|
||||
const allowedMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
|
||||
if (!allowedMimeTypes.includes(req.file.mimetype)) {
|
||||
return res.status(400).json({
|
||||
status: 'error',
|
||||
message: 'Invalid file type. Only images are allowed.'
|
||||
});
|
||||
}
|
||||
|
||||
// Validate file size (5MB max)
|
||||
if (req.file.size > 5 * 1024 * 1024) {
|
||||
return res.status(400).json({
|
||||
status: 'error',
|
||||
message: 'File too large. Maximum size is 5MB.'
|
||||
});
|
||||
}
|
||||
|
||||
// Sanitize filename
|
||||
req.file.originalname = req.file.originalname
|
||||
.replace(/[^a-zA-Z0-9.-]/g, '_')
|
||||
.substring(0, 100);
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
// Core sanitization middleware
|
||||
sanitizeMongoData,
|
||||
sanitizeXSS,
|
||||
preventHPP,
|
||||
sanitizeInput,
|
||||
sanitizeAll,
|
||||
|
||||
// Specific field validators
|
||||
sanitizeEmail,
|
||||
sanitizePassword,
|
||||
sanitizeUsername,
|
||||
sanitizeId,
|
||||
sanitizePagination,
|
||||
sanitizeSearch,
|
||||
sanitizeQuizParams,
|
||||
|
||||
// Validation handler
|
||||
handleValidationErrors,
|
||||
|
||||
// File upload sanitization
|
||||
sanitizeFileUpload
|
||||
};
|
||||
Reference in New Issue
Block a user