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 avatar
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