/** * HVAC Testing Framework - Secure Input Validation * * Provides comprehensive input validation and sanitization for all data entry points * to prevent injection attacks, XSS, and data corruption vulnerabilities. * * Security Features: * - SQL injection prevention * - XSS prevention with content sanitization * - Command injection prevention * - WordPress-specific validation patterns * - File upload security validation * - URL and email validation with security checks * * @author Claude Code - Emergency Security Response * @version 1.0.0 * @security CRITICAL - Prevents injection and data validation vulnerabilities */ const crypto = require('crypto'); const validator = require('validator'); const { getCredentialManager } = require('./SecureCredentialManager'); class SecureInputValidator { constructor() { this.credentialManager = getCredentialManager(); this.validationPatterns = this.initializeValidationPatterns(); this.sanitizationRules = this.initializeSanitizationRules(); this.validationLog = []; } /** * Initialize validation patterns for different data types * @returns {Map} Validation patterns */ initializeValidationPatterns() { return new Map([ // WordPress-specific patterns ['wp_username', /^[a-zA-Z0-9@._-]{1,60}$/], ['wp_email', /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/], ['wp_slug', /^[a-z0-9-]+$/], ['wp_role', /^(administrator|hvac_master_trainer|hvac_trainer|subscriber)$/], ['wp_nonce', /^[a-f0-9]{10}$/], ['wp_capability', /^[a-z_]+$/], // URL and path patterns ['safe_url', /^https?:\/\/[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(\/[a-zA-Z0-9._~:/?#[\]@!$&'()*+,;=-]*)?$/], ['relative_path', /^\/[a-zA-Z0-9._~:/?#[\]@!$&'()*+,;=-]*$/], ['file_path', /^[a-zA-Z0-9._/-]+$/], // Database and query patterns ['table_name', /^[a-zA-Z_][a-zA-Z0-9_]*$/], ['column_name', /^[a-zA-Z_][a-zA-Z0-9_]*$/], ['order_direction', /^(ASC|DESC)$/i], ['limit_value', /^\d+$/], // Form field patterns ['text_field', /^[a-zA-Z0-9\s.,!?-]{0,1000}$/], ['name_field', /^[a-zA-Z\s-]{1,100}$/], ['phone_field', /^[\+]?[1-9][\d]{0,15}$/], ['alphanumeric', /^[a-zA-Z0-9]+$/], ['numeric', /^\d+$/], // Security-specific patterns ['session_id', /^[a-f0-9-]{36}$/], ['csrf_token', /^[a-zA-Z0-9+/=]{32,}$/], ['api_key', /^[a-zA-Z0-9]{32,}$/], // File and upload patterns ['safe_filename', /^[a-zA-Z0-9._-]+\.(jpg|jpeg|png|gif|pdf|doc|docx|txt)$/i], ['image_filename', /^[a-zA-Z0-9._-]+\.(jpg|jpeg|png|gif)$/i], ['document_filename', /^[a-zA-Z0-9._-]+\.(pdf|doc|docx|txt)$/i] ]); } /** * Initialize sanitization rules for different contexts * @returns {Map} Sanitization rules */ initializeSanitizationRules() { return new Map([ // HTML sanitization ['html', { allowedTags: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li'], stripScripts: true, stripStyles: true, stripComments: true }], // SQL sanitization ['sql', { escapeQuotes: true, stripComments: true, allowedChars: /^[a-zA-Z0-9\s.,_-]*$/ }], // JavaScript sanitization ['js', { stripScripts: true, stripEvents: true, stripEval: true }], // WordPress content sanitization ['wp_content', { allowedTags: ['p', 'br', 'strong', 'em', 'a', 'ul', 'ol', 'li'], allowedAttributes: ['href', 'title'], stripScripts: true }] ]); } /** * Validate input against a specific pattern * @param {string} input - Input to validate * @param {string} patternName - Pattern name from validationPatterns * @param {Object} options - Validation options * @returns {Object} Validation result */ validate(input, patternName, options = {}) { const validationId = this.generateValidationId(); const startTime = Date.now(); try { // Basic input checks if (input === null || input === undefined) { throw new Error('Input cannot be null or undefined'); } // Convert to string for validation const inputStr = String(input).trim(); // Length checks if (options.minLength && inputStr.length < options.minLength) { throw new Error(`Input too short (minimum ${options.minLength} characters)`); } if (options.maxLength && inputStr.length > options.maxLength) { throw new Error(`Input too long (maximum ${options.maxLength} characters)`); } // Pattern validation const pattern = this.validationPatterns.get(patternName); if (!pattern) { throw new Error(`Unknown validation pattern: ${patternName}`); } if (!pattern.test(inputStr)) { throw new Error(`Input does not match required pattern: ${patternName}`); } // Additional security checks this.performSecurityChecks(inputStr, patternName); // Log successful validation this.logValidation(validationId, patternName, 'SUCCESS', { inputLength: inputStr.length, duration: Date.now() - startTime }); return { valid: true, sanitized: inputStr, pattern: patternName, validationId }; } catch (error) { // Log failed validation this.logValidation(validationId, patternName, 'FAILED', { error: error.message, inputLength: input ? String(input).length : 0, duration: Date.now() - startTime }); return { valid: false, error: error.message, pattern: patternName, validationId }; } } /** * Sanitize input for a specific context * @param {string} input - Input to sanitize * @param {string} context - Sanitization context * @returns {string} Sanitized input */ sanitize(input, context) { if (!input) return ''; const rules = this.sanitizationRules.get(context); if (!rules) { throw new Error(`Unknown sanitization context: ${context}`); } let sanitized = String(input).trim(); switch (context) { case 'html': sanitized = this.sanitizeHTML(sanitized, rules); break; case 'sql': sanitized = this.sanitizeSQL(sanitized, rules); break; case 'js': sanitized = this.sanitizeJavaScript(sanitized, rules); break; case 'wp_content': sanitized = this.sanitizeWordPressContent(sanitized, rules); break; default: sanitized = this.sanitizeGeneric(sanitized); } this.logSanitization(context, input.length, sanitized.length); return sanitized; } /** * Validate and sanitize input in one operation * @param {string} input - Input to process * @param {string} patternName - Validation pattern * @param {string} sanitizeContext - Sanitization context * @param {Object} options - Processing options * @returns {Object} Processing result */ validateAndSanitize(input, patternName, sanitizeContext, options = {}) { // First sanitize to remove potential threats const sanitized = this.sanitize(input, sanitizeContext); // Then validate the sanitized input const validation = this.validate(sanitized, patternName, options); return { ...validation, originalInput: input, sanitizedInput: sanitized }; } /** * Perform additional security checks on input * @param {string} input - Input to check * @param {string} patternName - Pattern being validated */ performSecurityChecks(input, patternName) { // Check for common injection patterns const dangerousPatterns = [ // SQL Injection /('|(\\')|(;|;$)|(\'|\"|\`)/, /(union|select|insert|update|delete|drop|create|alter|exec|execute)/i, // XSS patterns /]*>.*?<\/script>/gi, /]*>.*?<\/iframe>/gi, /javascript:/gi, /on\w+\s*=/gi, // Command injection /[;&|`$(){}]/, /(rm|del|format|shutdown|reboot)/i, // Path traversal /\.\.[\/\\]/, /(\/etc\/passwd|\/bin\/sh|cmd\.exe|powershell\.exe)/i, // LDAP injection /[()&|!]/, // NoSQL injection /(\$where|\$ne|\$gt|\$lt|\$in|\$nin)/i ]; for (const pattern of dangerousPatterns) { if (pattern.test(input)) { throw new Error('Input contains potentially dangerous patterns'); } } // Context-specific checks if (patternName === 'wp_username' && this.isReservedWordPressUsername(input)) { throw new Error('Username is reserved'); } if (patternName === 'safe_url' && !this.isAllowedDomain(input)) { throw new Error('URL domain not allowed'); } } /** * HTML sanitization * @param {string} input - HTML input * @param {Object} rules - Sanitization rules * @returns {string} Sanitized HTML */ sanitizeHTML(input, rules) { let sanitized = input; // Remove script tags and content if (rules.stripScripts) { sanitized = sanitized.replace(/]*>.*?<\/script>/gsi, ''); sanitized = sanitized.replace(/javascript:/gi, ''); } // Remove style tags and attributes if (rules.stripStyles) { sanitized = sanitized.replace(/]*>.*?<\/style>/gsi, ''); sanitized = sanitized.replace(/style\s*=\s*["'][^"']*["']/gi, ''); } // Remove comments if (rules.stripComments) { sanitized = sanitized.replace(//gs, ''); } // Remove event handlers sanitized = sanitized.replace(/on\w+\s*=\s*["'][^"']*["']/gi, ''); // Encode special characters sanitized = sanitized .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); return sanitized; } /** * SQL sanitization * @param {string} input - SQL input * @param {Object} rules - Sanitization rules * @returns {string} Sanitized SQL */ sanitizeSQL(input, rules) { let sanitized = input; // Escape quotes if (rules.escapeQuotes) { sanitized = sanitized.replace(/'/g, "''"); sanitized = sanitized.replace(/"/g, '""'); } // Remove SQL comments if (rules.stripComments) { sanitized = sanitized.replace(/--.*$/gm, ''); sanitized = sanitized.replace(/\/\*.*?\*\//gs, ''); } // Validate allowed characters if (rules.allowedChars && !rules.allowedChars.test(sanitized)) { throw new Error('SQL input contains invalid characters'); } return sanitized; } /** * JavaScript sanitization * @param {string} input - JavaScript input * @param {Object} rules - Sanitization rules * @returns {string} Sanitized JavaScript */ sanitizeJavaScript(input, rules) { let sanitized = input; if (rules.stripScripts) { sanitized = sanitized.replace(/]*>.*?<\/script>/gsi, ''); } if (rules.stripEvents) { sanitized = sanitized.replace(/on\w+\s*=\s*["'][^"']*["']/gi, ''); } if (rules.stripEval) { sanitized = sanitized.replace(/eval\s*\(/gi, ''); } return sanitized; } /** * WordPress content sanitization * @param {string} input - WordPress content * @param {Object} rules - Sanitization rules * @returns {string} Sanitized content */ sanitizeWordPressContent(input, rules) { // First apply HTML sanitization let sanitized = this.sanitizeHTML(input, rules); // WordPress-specific cleaning sanitized = sanitized.replace(/\[shortcode[^\]]*\]/gi, ''); // Remove shortcodes sanitized = sanitized.replace(/\{\{.*?\}\}/g, ''); // Remove template syntax return sanitized; } /** * Generic sanitization for unknown contexts * @param {string} input - Input to sanitize * @returns {string} Sanitized input */ sanitizeGeneric(input) { return input .replace(/[<>&"']/g, (match) => { const entities = { '<': '<', '>': '>', '&': '&', '"': '"', "'": ''' }; return entities[match] || match; }) .trim(); } /** * Validate file uploads * @param {Object} fileInfo - File information * @returns {Object} Validation result */ validateFileUpload(fileInfo) { const { filename, size, mimetype } = fileInfo; // Validate filename const filenameValidation = this.validate(filename, 'safe_filename'); if (!filenameValidation.valid) { return filenameValidation; } // Size limits const maxSize = 10 * 1024 * 1024; // 10MB if (size > maxSize) { return { valid: false, error: 'File too large (maximum 10MB)' }; } // MIME type validation const allowedMimes = [ 'image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'text/plain' ]; if (!allowedMimes.includes(mimetype)) { return { valid: false, error: 'File type not allowed' }; } return { valid: true, filename: filenameValidation.sanitized }; } /** * WordPress-specific validation helpers */ /** * Validate WordPress nonce * @param {string} nonce - Nonce value * @param {string} action - Action name * @returns {boolean} Validation result */ validateWordPressNonce(nonce, action) { const validation = this.validate(nonce, 'wp_nonce'); if (!validation.valid) { return false; } // Additional WordPress nonce validation would go here // This would typically verify against WordPress's nonce system return true; } /** * Validate WordPress capability * @param {string} capability - Capability name * @returns {boolean} Validation result */ validateWordPressCapability(capability) { const allowedCapabilities = [ 'read', 'edit_posts', 'edit_others_posts', 'publish_posts', 'manage_options', 'manage_hvac_trainers', 'manage_hvac_events', 'view_hvac_reports' ]; return allowedCapabilities.includes(capability); } /** * Utility methods */ generateValidationId() { return crypto.randomUUID(); } isReservedWordPressUsername(username) { const reserved = ['admin', 'administrator', 'root', 'test', 'guest', 'user']; return reserved.includes(username.toLowerCase()); } isAllowedDomain(url) { try { const urlObj = new URL(url); const allowedDomains = [ 'measurequick.com', 'upskill-staging.measurequick.com' ]; return allowedDomains.some(domain => urlObj.hostname.endsWith(domain)); } catch { return false; } } logValidation(validationId, pattern, status, details) { const logEntry = { validationId, timestamp: new Date().toISOString(), pattern, status, ...details }; this.validationLog.push(logEntry); this.credentialManager.auditLogger('INPUT_VALIDATION', logEntry); } logSanitization(context, originalLength, sanitizedLength) { this.credentialManager.auditLogger('INPUT_SANITIZATION', { context, originalLength, sanitizedLength, reductionRatio: ((originalLength - sanitizedLength) / originalLength) * 100 }); } /** * Get validation audit log * @returns {Array} Validation log */ getValidationLog() { return [...this.validationLog]; } /** * Clear validation log */ clearValidationLog() { this.validationLog = []; } } // Singleton instance let inputValidatorInstance = null; /** * Get singleton instance of SecureInputValidator * @returns {SecureInputValidator} */ function getInputValidator() { if (!inputValidatorInstance) { inputValidatorInstance = new SecureInputValidator(); } return inputValidatorInstance; } module.exports = { SecureInputValidator, getInputValidator };