Express.js Input Validation Middleware
This is a custom middleware using express-validator to handle schema validation errors for my API endpoints. It logs failed validation attempts for security monitoring and returns a structured 400 error response to the client.user_3179742
javascript
02/21/2026 7:39 AM
8.4 KB
9
Indexable
// b/src/shared/middleware/inputValidation.js
// Purpose: Input validation middleware for security and data integrity
import { body, param, query, validationResult } from 'express-validator';
/**
* Input Validation Middleware
* Provides comprehensive input validation for API endpoints
*/
// Custom validation error handler
export const handleValidationErrors = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
// Log validation errors for security monitoring
console.warn('Input validation failed:', {
url: req.url,
method: req.method,
ip: req.ip,
errors: errors.array().map(e => ({ field: e.path, value: e.value, msg: e.msg })),
userAgent: req.get('User-Agent')
});
return res.status(400).json({
error: 'Invalid input data',
details: errors.array().map(e => ({ field: e.path, message: e.msg }))
});
}
next();
};
// Common validation rules
export const commonValidations = {
// Email validation
email: body('email')
.isEmail()
.withMessage('Please provide a valid email address')
.normalizeEmail()
.isLength({ max: 255 })
.withMessage('Email address is too long'),
// Password validation
password: body('password')
.isLength({ min: 8, max: 128 })
.withMessage('Password must be between 8 and 128 characters')
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/)
.withMessage('Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character'),
// Name validation
name: body('name')
.isLength({ min: 2, max: 100 })
.withMessage('Name must be between 2 and 100 characters')
.matches(/^[a-zA-Z\s'-]+$/)
.withMessage('Name can only contain letters, spaces, hyphens, and apostrophes'),
// Phone number validation
phoneNumber: body('phoneNumber')
.optional()
.isMobilePhone('any', { strictMode: false })
.withMessage('Please provide a valid phone number'),
// Token validation (6-digit codes)
token: body('token')
.isLength({ min: 6, max: 6 })
.withMessage('Verification code must be exactly 6 digits')
.isNumeric()
.withMessage('Verification code must contain only numbers'),
// ID validation for MongoDB ObjectId
objectId: (field) => param(field)
.isMongoId()
.withMessage(`Invalid ${field} ID format`),
// Pagination validation
page: query('page')
.optional()
.isInt({ min: 1 })
.withMessage('Page must be a positive integer'),
limit: query('limit')
.optional()
.isInt({ min: 1, max: 100 })
.withMessage('Limit must be between 1 and 100'),
// Search query validation
search: query('search')
.optional()
.isLength({ min: 1, max: 100 })
.withMessage('Search query must be between 1 and 100 characters')
.escape()
.withMessage('Search query contains invalid characters')
};
// Specific validation chains for different endpoints
export const validations = {
// Registration validation
register: [
commonValidations.email,
commonValidations.password,
commonValidations.name,
body('role')
.optional()
.isIn(['admin', 'clerk', 'manager', 'customer'])
.withMessage('Invalid role specified')
],
// Login validation
login: [
commonValidations.email,
body('password')
.notEmpty()
.withMessage('Password is required')
],
// Email verification
verifyEmail: [
commonValidations.email,
commonValidations.token
],
// Password reset request
forgotPassword: [
commonValidations.email
],
// Password reset confirmation
resetPassword: [
commonValidations.token,
commonValidations.password
],
// Profile update
updateProfile: [
commonValidations.name.optional(),
commonValidations.phoneNumber.optional(),
body('gender')
.optional()
.isIn(['Male', 'Female', 'Other'])
.withMessage('Invalid gender specified'),
body('dateOfBirth')
.optional()
.isISO8601()
.withMessage('Invalid date format')
.toDate()
.custom(value => {
if (value && value > new Date()) {
throw new Error('Date of birth cannot be in the future');
}
return true;
})
],
// User ID validation
userId: [
commonValidations.objectId('id')
],
// Product search
productSearch: [
commonValidations.search,
commonValidations.page,
commonValidations.limit,
query('category')
.optional()
.isMongoId()
.withMessage('Invalid category ID'),
query('minPrice')
.optional()
.isFloat({ min: 0 })
.withMessage('Minimum price must be a positive number'),
query('maxPrice')
.optional()
.isFloat({ min: 0 })
.withMessage('Maximum price must be a positive number')
]
};
// Sanitization middleware for XSS prevention
export const sanitizeInput = (req, res, next) => {
// Basic XSS sanitization for all string inputs
const sanitizeString = (str) => {
if (typeof str !== 'string') return str;
// Remove potentially dangerous characters
return str
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, '')
.replace(/javascript:/gi, '')
.replace(/on\w+\s*=/gi, '');
};
// Recursively sanitize object
const sanitizeObject = (obj) => {
if (Array.isArray(obj)) {
return obj.map(sanitizeObject);
} else if (obj && typeof obj === 'object') {
const sanitized = {};
for (const [key, value] of Object.entries(obj)) {
sanitized[key] = sanitizeObject(value);
}
return sanitized;
} else if (typeof obj === 'string') {
return sanitizeString(obj);
}
return obj;
};
// Sanitize request body, query, and params
if (req.body) {
req.body = sanitizeObject(req.body);
}
if (req.query) {
req.query = sanitizeObject(req.query);
}
if (req.params) {
req.params = sanitizeObject(req.params);
}
next();
};
export default {
handleValidationErrors,
commonValidations,
validations,
sanitizeInput
};
Editor is loading...
Leave a Comment