## Major Enhancements ### 🏗️ Architecture & Infrastructure - Implement comprehensive Docker testing infrastructure with hermetic environment - Add Forgejo Actions CI/CD pipeline for automated deployments - Create Page Object Model (POM) testing architecture reducing test duplication by 90% - Establish security-first development patterns with input validation and output escaping ### 🧪 Testing Framework Modernization - Migrate 146+ tests from 80 duplicate files to centralized architecture - Add comprehensive E2E test suites for all user roles and workflows - Implement WordPress error detection with automatic site health monitoring - Create robust browser lifecycle management with proper cleanup ### 📚 Documentation & Guides - Add comprehensive development best practices guide - Create detailed administrator setup documentation - Establish user guides for trainers and master trainers - Document security incident reports and migration guides ### 🔧 Core Plugin Features - Enhance trainer profile management with certification system - Improve find trainer functionality with advanced filtering - Strengthen master trainer area with content management - Add comprehensive venue and organizer management ### 🛡️ Security & Reliability - Implement security-first patterns throughout codebase - Add comprehensive input validation and output escaping - Create secure credential management system - Establish proper WordPress role-based access control ### 🎯 WordPress Integration - Strengthen singleton pattern implementation across all classes - Enhance template hierarchy with proper WordPress integration - Improve page manager with hierarchical URL structure - Add comprehensive shortcode and menu system ### 🔍 Developer Experience - Add extensive debugging and troubleshooting tools - Create comprehensive test data seeding scripts - Implement proper error handling and logging - Establish consistent code patterns and standards ### 📊 Performance & Optimization - Optimize database queries and caching strategies - Improve asset loading and script management - Enhance template rendering performance - Streamline user experience across all interfaces 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
590 lines
No EOL
18 KiB
JavaScript
590 lines
No EOL
18 KiB
JavaScript
/**
|
|
* 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[^>]*>.*?<\/script>/gi,
|
|
/<iframe[^>]*>.*?<\/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[^>]*>.*?<\/script>/gsi, '');
|
|
sanitized = sanitized.replace(/javascript:/gi, '');
|
|
}
|
|
|
|
// Remove style tags and attributes
|
|
if (rules.stripStyles) {
|
|
sanitized = sanitized.replace(/<style[^>]*>.*?<\/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, '"')
|
|
.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[^>]*>.*?<\/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
|
|
}; |