## 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>
649 lines
No EOL
22 KiB
JavaScript
649 lines
No EOL
22 KiB
JavaScript
/**
|
|
* HVAC Testing Framework - WordPress Security Helpers
|
|
*
|
|
* Provides WordPress-specific security validation, nonce verification,
|
|
* capability checking, and role management for secure testing.
|
|
*
|
|
* Security Features:
|
|
* - WordPress nonce validation and generation
|
|
* - User capability and role verification
|
|
* - CSRF protection for WordPress actions
|
|
* - SQL injection prevention for WordPress queries
|
|
* - Authentication state validation
|
|
*
|
|
* @author Claude Code - Emergency Security Response
|
|
* @version 1.0.0
|
|
* @security CRITICAL - WordPress-specific security validation
|
|
*/
|
|
|
|
const { getCredentialManager } = require('./SecureCredentialManager');
|
|
const { getInputValidator } = require('./SecureInputValidator');
|
|
const { getCommandExecutor } = require('./SecureCommandExecutor');
|
|
|
|
class WordPressSecurityHelpers {
|
|
constructor() {
|
|
this.credentialManager = getCredentialManager();
|
|
this.inputValidator = getInputValidator();
|
|
this.commandExecutor = getCommandExecutor();
|
|
this.nonceCache = new Map();
|
|
this.capabilityCache = new Map();
|
|
this.roleDefinitions = this.initializeRoleDefinitions();
|
|
}
|
|
|
|
/**
|
|
* Initialize WordPress role definitions for HVAC system
|
|
* @returns {Map} Role definitions with capabilities
|
|
*/
|
|
initializeRoleDefinitions() {
|
|
return new Map([
|
|
['administrator', {
|
|
capabilities: [
|
|
'manage_options', 'edit_posts', 'edit_others_posts', 'publish_posts',
|
|
'edit_pages', 'edit_others_pages', 'publish_pages', 'delete_posts',
|
|
'delete_others_posts', 'delete_pages', 'delete_others_pages',
|
|
'manage_categories', 'manage_links', 'upload_files', 'edit_files',
|
|
'manage_users', 'install_plugins', 'activate_plugins', 'switch_themes',
|
|
'edit_themes', 'edit_users', 'delete_users', 'unfiltered_html',
|
|
'manage_hvac_system', 'manage_hvac_trainers', 'manage_hvac_events',
|
|
'view_hvac_reports', 'export_hvac_data', 'import_hvac_data'
|
|
],
|
|
level: 10,
|
|
description: 'Full system administrator'
|
|
}],
|
|
|
|
['hvac_master_trainer', {
|
|
capabilities: [
|
|
'read', 'edit_posts', 'publish_posts', 'upload_files',
|
|
'manage_hvac_trainers', 'manage_hvac_events', 'view_hvac_reports',
|
|
'export_hvac_data', 'import_hvac_data', 'edit_trainer_profiles',
|
|
'approve_trainers', 'create_announcements', 'manage_communications',
|
|
'view_pending_approvals', 'access_master_dashboard'
|
|
],
|
|
level: 8,
|
|
description: 'Master trainer with management capabilities'
|
|
}],
|
|
|
|
['hvac_trainer', {
|
|
capabilities: [
|
|
'read', 'edit_own_posts', 'upload_files',
|
|
'create_hvac_events', 'edit_own_events', 'manage_own_venues',
|
|
'manage_own_organizers', 'view_training_leads', 'edit_own_profile',
|
|
'access_trainer_dashboard', 'submit_for_approval'
|
|
],
|
|
level: 5,
|
|
description: 'Regular trainer with event management'
|
|
}],
|
|
|
|
['subscriber', {
|
|
capabilities: ['read'],
|
|
level: 0,
|
|
description: 'Basic read-only access'
|
|
}]
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Verify WordPress nonce for CSRF protection
|
|
* @param {string} nonce - Nonce value to verify
|
|
* @param {string} action - Action the nonce was generated for
|
|
* @param {Object} context - Additional context (user, timestamp, etc.)
|
|
* @returns {Promise<Object>} Verification result
|
|
*/
|
|
async verifyWordPressNonce(nonce, action, context = {}) {
|
|
try {
|
|
// Basic nonce format validation
|
|
const validation = this.inputValidator.validate(nonce, 'wp_nonce');
|
|
if (!validation.valid) {
|
|
throw new Error(`Invalid nonce format: ${validation.error}`);
|
|
}
|
|
|
|
// Check nonce cache for recent verifications
|
|
const cacheKey = `${nonce}-${action}`;
|
|
if (this.nonceCache.has(cacheKey)) {
|
|
const cached = this.nonceCache.get(cacheKey);
|
|
if (Date.now() - cached.timestamp < 300000) { // 5 minutes
|
|
return { valid: true, cached: true, ...cached };
|
|
}
|
|
this.nonceCache.delete(cacheKey);
|
|
}
|
|
|
|
// Verify nonce via WordPress CLI
|
|
const wpCommand = `eval "echo wp_verify_nonce('${nonce}', '${action}');"`;
|
|
const result = await this.commandExecutor.executeWordPressCommand(wpCommand);
|
|
|
|
const isValid = result.stdout.trim() === '1';
|
|
|
|
// Cache result
|
|
if (isValid) {
|
|
this.nonceCache.set(cacheKey, {
|
|
valid: true,
|
|
action,
|
|
timestamp: Date.now(),
|
|
context
|
|
});
|
|
}
|
|
|
|
await this.credentialManager.auditLogger('NONCE_VERIFICATION', {
|
|
nonce: nonce.substring(0, 6) + '...',
|
|
action,
|
|
valid: isValid,
|
|
context
|
|
});
|
|
|
|
return {
|
|
valid: isValid,
|
|
action,
|
|
verified_at: new Date().toISOString()
|
|
};
|
|
|
|
} catch (error) {
|
|
await this.credentialManager.auditLogger('NONCE_VERIFICATION_ERROR', {
|
|
action,
|
|
error: error.message,
|
|
context
|
|
});
|
|
|
|
return {
|
|
valid: false,
|
|
error: error.message
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate WordPress nonce for an action
|
|
* @param {string} action - Action to generate nonce for
|
|
* @param {Object} context - Generation context
|
|
* @returns {Promise<string>} Generated nonce
|
|
*/
|
|
async generateWordPressNonce(action, context = {}) {
|
|
try {
|
|
const wpCommand = `eval "echo wp_create_nonce('${action}');"`;
|
|
const result = await this.commandExecutor.executeWordPressCommand(wpCommand);
|
|
|
|
const nonce = result.stdout.trim();
|
|
|
|
// Validate generated nonce
|
|
const validation = this.inputValidator.validate(nonce, 'wp_nonce');
|
|
if (!validation.valid) {
|
|
throw new Error('Generated nonce is invalid');
|
|
}
|
|
|
|
// Cache the generated nonce
|
|
const cacheKey = `${nonce}-${action}`;
|
|
this.nonceCache.set(cacheKey, {
|
|
valid: true,
|
|
action,
|
|
timestamp: Date.now(),
|
|
generated: true,
|
|
context
|
|
});
|
|
|
|
await this.credentialManager.auditLogger('NONCE_GENERATED', {
|
|
action,
|
|
nonce: nonce.substring(0, 6) + '...',
|
|
context
|
|
});
|
|
|
|
return nonce;
|
|
|
|
} catch (error) {
|
|
await this.credentialManager.auditLogger('NONCE_GENERATION_ERROR', {
|
|
action,
|
|
error: error.message,
|
|
context
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify user has required capability
|
|
* @param {number|string} userId - WordPress user ID or username
|
|
* @param {string} capability - Required capability
|
|
* @returns {Promise<Object>} Capability check result
|
|
*/
|
|
async verifyUserCapability(userId, capability) {
|
|
try {
|
|
// Validate capability name
|
|
const validation = this.inputValidator.validate(capability, 'wp_capability');
|
|
if (!validation.valid) {
|
|
throw new Error(`Invalid capability name: ${validation.error}`);
|
|
}
|
|
|
|
// Check capability cache
|
|
const cacheKey = `${userId}-${capability}`;
|
|
if (this.capabilityCache.has(cacheKey)) {
|
|
const cached = this.capabilityCache.get(cacheKey);
|
|
if (Date.now() - cached.timestamp < 600000) { // 10 minutes
|
|
return { ...cached, cached: true };
|
|
}
|
|
this.capabilityCache.delete(cacheKey);
|
|
}
|
|
|
|
// Get user data via WordPress CLI
|
|
const userCommand = `user get ${userId} --fields=roles,allcaps --format=json`;
|
|
const userResult = await this.commandExecutor.executeWordPressCommand(userCommand);
|
|
|
|
const userData = JSON.parse(userResult.stdout);
|
|
const hasCapability = userData.allcaps && userData.allcaps[capability] === true;
|
|
|
|
// Cache result
|
|
const result = {
|
|
userId,
|
|
capability,
|
|
hasCapability,
|
|
userRoles: userData.roles || [],
|
|
timestamp: Date.now()
|
|
};
|
|
|
|
this.capabilityCache.set(cacheKey, result);
|
|
|
|
await this.credentialManager.auditLogger('CAPABILITY_CHECK', {
|
|
userId,
|
|
capability,
|
|
hasCapability,
|
|
roles: userData.roles
|
|
});
|
|
|
|
return result;
|
|
|
|
} catch (error) {
|
|
await this.credentialManager.auditLogger('CAPABILITY_CHECK_ERROR', {
|
|
userId,
|
|
capability,
|
|
error: error.message
|
|
});
|
|
|
|
return {
|
|
userId,
|
|
capability,
|
|
hasCapability: false,
|
|
error: error.message
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify user has required role
|
|
* @param {number|string} userId - WordPress user ID or username
|
|
* @param {string|Array} requiredRoles - Required role(s)
|
|
* @returns {Promise<Object>} Role verification result
|
|
*/
|
|
async verifyUserRole(userId, requiredRoles) {
|
|
try {
|
|
const roles = Array.isArray(requiredRoles) ? requiredRoles : [requiredRoles];
|
|
|
|
// Validate role names
|
|
for (const role of roles) {
|
|
const validation = this.inputValidator.validate(role, 'wp_role');
|
|
if (!validation.valid) {
|
|
throw new Error(`Invalid role name: ${role}`);
|
|
}
|
|
}
|
|
|
|
// Get user roles
|
|
const userCommand = `user get ${userId} --fields=roles --format=json`;
|
|
const result = await this.commandExecutor.executeWordPressCommand(userCommand);
|
|
|
|
const userData = JSON.parse(result.stdout);
|
|
const userRoles = userData.roles || [];
|
|
|
|
// Check if user has any of the required roles
|
|
const hasRequiredRole = roles.some(role => userRoles.includes(role));
|
|
const matchedRoles = roles.filter(role => userRoles.includes(role));
|
|
|
|
await this.credentialManager.auditLogger('ROLE_VERIFICATION', {
|
|
userId,
|
|
requiredRoles: roles,
|
|
userRoles,
|
|
hasRequiredRole,
|
|
matchedRoles
|
|
});
|
|
|
|
return {
|
|
userId,
|
|
requiredRoles: roles,
|
|
userRoles,
|
|
hasRequiredRole,
|
|
matchedRoles
|
|
};
|
|
|
|
} catch (error) {
|
|
await this.credentialManager.auditLogger('ROLE_VERIFICATION_ERROR', {
|
|
userId,
|
|
requiredRoles,
|
|
error: error.message
|
|
});
|
|
|
|
return {
|
|
userId,
|
|
requiredRoles,
|
|
hasRequiredRole: false,
|
|
error: error.message
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate WordPress database query for security
|
|
* @param {string} query - SQL query to validate
|
|
* @param {Object} context - Query context
|
|
* @returns {Object} Query validation result
|
|
*/
|
|
validateWordPressQuery(query, context = {}) {
|
|
try {
|
|
// Basic SQL injection prevention
|
|
const sanitized = this.inputValidator.sanitize(query, 'sql');
|
|
const validation = this.inputValidator.validate(sanitized, 'text_field', {
|
|
maxLength: 10000
|
|
});
|
|
|
|
if (!validation.valid) {
|
|
throw new Error(`Query validation failed: ${validation.error}`);
|
|
}
|
|
|
|
// WordPress-specific query validation
|
|
this.validateWordPressQueryStructure(sanitized);
|
|
|
|
this.credentialManager.auditLogger('QUERY_VALIDATION', {
|
|
queryHash: require('crypto').createHash('sha256').update(query).digest('hex'),
|
|
context,
|
|
valid: true
|
|
});
|
|
|
|
return {
|
|
valid: true,
|
|
sanitizedQuery: sanitized
|
|
};
|
|
|
|
} catch (error) {
|
|
this.credentialManager.auditLogger('QUERY_VALIDATION_ERROR', {
|
|
error: error.message,
|
|
context
|
|
});
|
|
|
|
return {
|
|
valid: false,
|
|
error: error.message
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate WordPress query structure
|
|
* @param {string} query - Query to validate
|
|
*/
|
|
validateWordPressQueryStructure(query) {
|
|
const normalizedQuery = query.trim().toLowerCase();
|
|
|
|
// Only allow SELECT queries for security
|
|
if (!normalizedQuery.startsWith('select')) {
|
|
throw new Error('Only SELECT queries are allowed');
|
|
}
|
|
|
|
// Block dangerous SQL operations
|
|
const forbiddenOperations = [
|
|
'drop', 'delete', 'truncate', 'alter', 'create',
|
|
'insert', 'update', 'grant', 'revoke', 'exec',
|
|
'execute', 'sp_', 'xp_'
|
|
];
|
|
|
|
for (const operation of forbiddenOperations) {
|
|
if (normalizedQuery.includes(operation)) {
|
|
throw new Error(`SQL operation not allowed: ${operation}`);
|
|
}
|
|
}
|
|
|
|
// Validate table names (must be WordPress tables)
|
|
const wpTables = [
|
|
'wp_posts', 'wp_users', 'wp_usermeta', 'wp_options',
|
|
'wp_postmeta', 'wp_comments', 'wp_commentmeta',
|
|
'wp_hvac_trainers', 'wp_hvac_events', 'wp_hvac_venues'
|
|
];
|
|
|
|
// Extract table names from query
|
|
const tableMatches = query.match(/from\s+(\w+)/gi);
|
|
if (tableMatches) {
|
|
for (const match of tableMatches) {
|
|
const tableName = match.replace(/from\s+/i, '').trim();
|
|
if (!wpTables.some(allowed => tableName.includes(allowed))) {
|
|
throw new Error(`Table not allowed: ${tableName}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate authentication state for a page/context
|
|
* @param {Object} page - Playwright page object
|
|
* @param {string} expectedRole - Expected user role
|
|
* @returns {Promise<Object>} Authentication validation result
|
|
*/
|
|
async validateAuthenticationState(page, expectedRole) {
|
|
try {
|
|
const currentUrl = page.url();
|
|
|
|
// Check if on login page (indicates not authenticated)
|
|
if (currentUrl.includes('/wp-login.php') || currentUrl.includes('/training-login/')) {
|
|
return {
|
|
authenticated: false,
|
|
reason: 'Currently on login page',
|
|
currentUrl
|
|
};
|
|
}
|
|
|
|
// Role-specific URL validation
|
|
const roleUrlMap = {
|
|
'hvac_master_trainer': /\/master-trainer\//,
|
|
'hvac_trainer': /\/trainer\//,
|
|
'administrator': /\/wp-admin\//
|
|
};
|
|
|
|
if (expectedRole && roleUrlMap[expectedRole]) {
|
|
if (!roleUrlMap[expectedRole].test(currentUrl)) {
|
|
return {
|
|
authenticated: false,
|
|
reason: `URL does not match expected role: ${expectedRole}`,
|
|
currentUrl,
|
|
expectedPattern: roleUrlMap[expectedRole].toString()
|
|
};
|
|
}
|
|
}
|
|
|
|
// Check for authentication indicators in page content
|
|
const bodyText = await page.textContent('body').catch(() => '');
|
|
|
|
// Negative indicators (suggests not authenticated)
|
|
const negativeIndicators = [
|
|
'please log in',
|
|
'login required',
|
|
'access denied',
|
|
'unauthorized',
|
|
'permission denied'
|
|
];
|
|
|
|
for (const indicator of negativeIndicators) {
|
|
if (bodyText.toLowerCase().includes(indicator)) {
|
|
return {
|
|
authenticated: false,
|
|
reason: `Negative authentication indicator found: ${indicator}`,
|
|
currentUrl
|
|
};
|
|
}
|
|
}
|
|
|
|
// Positive indicators (suggests authenticated)
|
|
const positiveIndicators = {
|
|
'hvac_master_trainer': ['master dashboard', 'manage trainers', 'announcements'],
|
|
'hvac_trainer': ['trainer dashboard', 'manage events', 'my venues'],
|
|
'administrator': ['dashboard', 'admin', 'settings']
|
|
};
|
|
|
|
if (expectedRole && positiveIndicators[expectedRole]) {
|
|
const hasPositiveIndicator = positiveIndicators[expectedRole]
|
|
.some(indicator => bodyText.toLowerCase().includes(indicator));
|
|
|
|
if (!hasPositiveIndicator) {
|
|
return {
|
|
authenticated: 'uncertain',
|
|
reason: 'No positive authentication indicators found',
|
|
currentUrl
|
|
};
|
|
}
|
|
}
|
|
|
|
await this.credentialManager.auditLogger('AUTHENTICATION_VALIDATION', {
|
|
expectedRole,
|
|
currentUrl,
|
|
authenticated: true
|
|
});
|
|
|
|
return {
|
|
authenticated: true,
|
|
role: expectedRole,
|
|
currentUrl
|
|
};
|
|
|
|
} catch (error) {
|
|
await this.credentialManager.auditLogger('AUTHENTICATION_VALIDATION_ERROR', {
|
|
expectedRole,
|
|
error: error.message
|
|
});
|
|
|
|
return {
|
|
authenticated: false,
|
|
error: error.message
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create CSRF-protected form data
|
|
* @param {Object} formData - Form data to protect
|
|
* @param {string} action - WordPress action
|
|
* @returns {Promise<Object>} Protected form data
|
|
*/
|
|
async createProtectedFormData(formData, action) {
|
|
try {
|
|
// Generate nonce for the action
|
|
const nonce = await this.generateWordPressNonce(action);
|
|
|
|
// Sanitize all form data
|
|
const protectedData = {};
|
|
for (const [key, value] of Object.entries(formData)) {
|
|
if (typeof value === 'string') {
|
|
protectedData[key] = this.inputValidator.sanitize(value, 'wp_content');
|
|
} else {
|
|
protectedData[key] = value;
|
|
}
|
|
}
|
|
|
|
// Add security fields
|
|
protectedData._wpnonce = nonce;
|
|
protectedData.action = action;
|
|
|
|
return {
|
|
success: true,
|
|
data: protectedData,
|
|
nonce
|
|
};
|
|
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: error.message
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get role capabilities
|
|
* @param {string} role - Role name
|
|
* @returns {Object} Role information
|
|
*/
|
|
getRoleCapabilities(role) {
|
|
const roleInfo = this.roleDefinitions.get(role);
|
|
if (!roleInfo) {
|
|
return {
|
|
exists: false,
|
|
error: `Unknown role: ${role}`
|
|
};
|
|
}
|
|
|
|
return {
|
|
exists: true,
|
|
role,
|
|
capabilities: roleInfo.capabilities,
|
|
level: roleInfo.level,
|
|
description: roleInfo.description
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Clean up caches (call periodically or on memory pressure)
|
|
*/
|
|
cleanupCaches() {
|
|
const now = Date.now();
|
|
|
|
// Clean nonce cache (older than 5 minutes)
|
|
for (const [key, value] of this.nonceCache) {
|
|
if (now - value.timestamp > 300000) {
|
|
this.nonceCache.delete(key);
|
|
}
|
|
}
|
|
|
|
// Clean capability cache (older than 10 minutes)
|
|
for (const [key, value] of this.capabilityCache) {
|
|
if (now - value.timestamp > 600000) {
|
|
this.capabilityCache.delete(key);
|
|
}
|
|
}
|
|
|
|
this.credentialManager.auditLogger('SECURITY_CACHE_CLEANUP', {
|
|
nonceCacheSize: this.nonceCache.size,
|
|
capabilityCacheSize: this.capabilityCache.size
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get security status summary
|
|
* @returns {Object} Security status
|
|
*/
|
|
getSecurityStatus() {
|
|
return {
|
|
timestamp: new Date().toISOString(),
|
|
caches: {
|
|
nonces: this.nonceCache.size,
|
|
capabilities: this.capabilityCache.size
|
|
},
|
|
roles: Array.from(this.roleDefinitions.keys()),
|
|
securityFeatures: [
|
|
'nonce_verification',
|
|
'capability_checking',
|
|
'role_validation',
|
|
'query_sanitization',
|
|
'auth_state_validation'
|
|
]
|
|
};
|
|
}
|
|
}
|
|
|
|
// Singleton instance
|
|
let wpSecurityHelpersInstance = null;
|
|
|
|
/**
|
|
* Get singleton instance of WordPressSecurityHelpers
|
|
* @returns {WordPressSecurityHelpers}
|
|
*/
|
|
function getWordPressSecurityHelpers() {
|
|
if (!wpSecurityHelpersInstance) {
|
|
wpSecurityHelpersInstance = new WordPressSecurityHelpers();
|
|
}
|
|
return wpSecurityHelpersInstance;
|
|
}
|
|
|
|
module.exports = {
|
|
WordPressSecurityHelpers,
|
|
getWordPressSecurityHelpers
|
|
}; |