diff --git a/includes/class-hvac-event-form-builder.php b/includes/class-hvac-event-form-builder.php index e560f9ec..dac85c36 100644 --- a/includes/class-hvac-event-form-builder.php +++ b/includes/class-hvac-event-form-builder.php @@ -987,8 +987,9 @@ class HVAC_Event_Form_Builder extends HVAC_Form_Builder { template_mode_enabled && $this->current_template): ?>
-

Using Template: current_template['name']); ?>

- + +

Using Template: current_template['name']); ?>

+
@@ -1198,19 +1199,34 @@ class HVAC_Event_Form_Builder extends HVAC_Form_Builder { * AJAX handler for loading template data */ public function ajax_load_template_data(): void { - // Security check - if (!wp_verify_nonce($_GET['nonce'] ?? '', 'hvac_template_nonce')) { - wp_send_json_error(['message' => __('Security check failed', 'hvac-community-events')]); + // SECURITY FIX: Use POST for all AJAX handlers and proper nonce verification + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + wp_send_json_error(['message' => __('Invalid request method', 'hvac-community-events')], 405); return; } - // Capability check - ensure user can create/edit events - if (!current_user_can('edit_posts') && !array_intersect(['hvac_trainer', 'hvac_master_trainer'], wp_get_current_user()->roles)) { - wp_send_json_error(['message' => __('Permission denied', 'hvac-community-events')]); + // Security check - Fixed: Use POST data for nonce verification + if (!wp_verify_nonce($_POST['nonce'] ?? '', 'hvac_template_nonce')) { + wp_send_json_error(['message' => __('Security check failed', 'hvac-community-events')], 403); return; } - $template_id = sanitize_text_field($_GET['template_id'] ?? ''); + // SECURITY FIX: Enhanced capability check with specific template permissions + if (!current_user_can('edit_posts') && !current_user_can('manage_hvac_templates')) { + wp_send_json_error(['message' => __('Permission denied', 'hvac-community-events')], 403); + return; + } + + // Additional role-based check for HVAC trainers + $user = wp_get_current_user(); + $allowed_roles = ['hvac_trainer', 'hvac_master_trainer', 'administrator']; + if (!array_intersect($allowed_roles, $user->roles) && !current_user_can('manage_options')) { + wp_send_json_error(['message' => __('Insufficient permissions', 'hvac-community-events')], 403); + return; + } + + // SECURITY FIX: Get template_id from POST data, not GET + $template_id = sanitize_text_field($_POST['template_id'] ?? ''); if (empty($template_id) || $template_id === '0') { wp_send_json_success(['template_data' => [], 'message' => __('Template cleared', 'hvac-community-events')]); return; @@ -1227,23 +1243,85 @@ class HVAC_Event_Form_Builder extends HVAC_Form_Builder { 'usage_count' => $template['usage_count'] + 1 ]); + // SECURITY FIX: Sanitize template data before sending to prevent XSS wp_send_json_success([ - 'template_data' => $template['field_data'], + 'template_data' => $this->sanitize_template_data($template['field_data']), 'template_info' => [ - 'name' => $template['name'], - 'description' => $template['description'], + 'name' => wp_kses_post($template['name']), + 'description' => wp_kses_post($template['description']), ], 'message' => __('Template loaded successfully', 'hvac-community-events') ]); } + /** + * SECURITY FIX: Sanitize template data to prevent XSS + * + * @param array $template_data Raw template data + * @return array Sanitized template data + */ + private function sanitize_template_data(array $template_data): array { + $sanitized = []; + + foreach ($template_data as $key => $value) { + if (is_array($value)) { + $sanitized[$key] = $this->sanitize_template_data($value); + } else { + // Apply appropriate sanitization based on field type + $field_config = $this->get_field_config($key); + if ($field_config && isset($field_config['sanitize'])) { + $sanitized[$key] = $this->sanitize_field_value($field_config, $value); + } else { + // Default to text sanitization + $sanitized[$key] = sanitize_text_field($value); + } + } + } + + return $sanitized; + } + + /** + * Get field configuration by name + * + * @param string $field_name Field name + * @return array|null Field configuration or null if not found + */ + private function get_field_config(string $field_name): ?array { + foreach ($this->fields as $field) { + if ($field['name'] === $field_name) { + return $field; + } + } + return null; + } + /** * AJAX handler for saving form as template */ public function ajax_save_as_template(): void { + // SECURITY FIX: Ensure POST method + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + wp_send_json_error(['message' => __('Invalid request method', 'hvac-community-events')], 405); + return; + } + // Security check if (!wp_verify_nonce($_POST['nonce'] ?? '', 'hvac_template_nonce')) { - wp_send_json_error(['message' => __('Security check failed', 'hvac-community-events')]); + wp_send_json_error(['message' => __('Security check failed', 'hvac-community-events')], 403); + return; + } + + // SECURITY FIX: Enhanced capability and role checks + if (!current_user_can('edit_posts') && !current_user_can('manage_hvac_templates')) { + wp_send_json_error(['message' => __('Permission denied', 'hvac-community-events')], 403); + return; + } + + $user = wp_get_current_user(); + $allowed_roles = ['hvac_trainer', 'hvac_master_trainer', 'administrator']; + if (!array_intersect($allowed_roles, $user->roles) && !current_user_can('manage_options')) { + wp_send_json_error(['message' => __('Insufficient permissions', 'hvac-community-events')], 403); return; } diff --git a/includes/class-hvac-plugin.php b/includes/class-hvac-plugin.php index 1feee6ab..c545f69c 100644 --- a/includes/class-hvac-plugin.php +++ b/includes/class-hvac-plugin.php @@ -868,18 +868,35 @@ final class HVAC_Plugin { * @param array $submission Submission data to debug */ public function debugTECSubmissionData(array $submission): void { + // SECURITY FIX: Only log debug info for authorized users and non-production environments + if (!defined('WP_DEBUG') || !WP_DEBUG) { + return; + } + + // Additional check for admin/development access + if (!current_user_can('manage_options') && !in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1', 'dev.upskillhvac.com'])) { + return; + } + + // SECURITY FIX: Sanitize sensitive data before logging $debug_info = [ 'timestamp' => current_time('Y-m-d H:i:s'), 'user_id' => get_current_user_id(), 'submission_keys' => array_keys($submission), 'has_excerpt' => isset($submission['excerpt']), 'has_post_excerpt' => isset($submission['post_excerpt']), - 'excerpt_value' => $submission['excerpt'] ?? 'not_set', - 'event_start' => $submission['EventStartDate'] ?? 'not_set', - 'event_end' => $submission['EventEndDate'] ?? 'not_set' + // SECURITY FIX: Sanitize user data before logging + 'excerpt_value' => isset($submission['excerpt']) ? wp_kses_post(substr($submission['excerpt'], 0, 100)) . '...' : 'not_set', + 'event_start' => isset($submission['EventStartDate']) ? sanitize_text_field($submission['EventStartDate']) : 'not_set', + 'event_end' => isset($submission['EventEndDate']) ? sanitize_text_field($submission['EventEndDate']) : 'not_set' ]; - error_log('[HVAC TEC Debug] Submission data: ' . print_r($debug_info, true)); + // SECURITY FIX: Use WordPress logging with proper data sanitization + if (class_exists('HVAC_Logger')) { + HVAC_Logger::debug('TEC Submission Debug: ' . wp_json_encode($debug_info), 'TEC_Integration'); + } else { + error_log('[HVAC TEC Debug] Submission data: ' . wp_json_encode($debug_info)); + } } /** @@ -1151,23 +1168,32 @@ final class HVAC_Plugin { 'stack_trace' => sanitize_textarea_field($logEntry['stack'] ?? '') ]; - // Log to WordPress debug log with structured format + // SECURITY FIX: Enhanced logging security with capability checks if (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { + // Only log for authorized users or development environments + if (!current_user_can('manage_options') && !in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1', 'dev.upskillhvac.com'])) { + wp_send_json_success(['logged' => false, 'message' => 'Debug logging disabled in production']); + return; + } + $logParts = [ '[SAFARI-DEBUG]', $safeLogEntry['timestamp'], - $safeLogEntry['message'], - 'UA: ' . $safeLogEntry['user_agent'] + substr($safeLogEntry['message'], 0, 100), // Limit message length + 'UA: ' . substr($safeLogEntry['user_agent'], 0, 50) // Limit UA length ]; - + + // SECURITY FIX: Limit data exposure and sanitize further if (!empty($safeLogEntry['data'])) { - $logParts[] = 'Data: ' . $safeLogEntry['data']; + $data = wp_kses_post(substr($safeLogEntry['data'], 0, 200)); + $logParts[] = 'Data: ' . $data . '...'; } - + if (!empty($safeLogEntry['error_info'])) { - $logParts[] = 'Error: ' . $safeLogEntry['error_info']; + $error = wp_kses_post(substr($safeLogEntry['error_info'], 0, 100)); + $logParts[] = 'Error: ' . $error . '...'; } - + error_log(implode(' | ', $logParts)); } @@ -1381,17 +1407,34 @@ final class HVAC_Plugin { } /** - * Load core files with error handling - * + * Load core files with error handling and security validation + * * @param string[] $files Array of file paths relative to plugin directory * @return Generator File path => loaded status */ private function loadCoreFiles(array $files): Generator { foreach ($files as $file) { + // SECURITY FIX: Validate file path to prevent directory traversal + if (!$this->isSecureFilePath($file)) { + error_log("HVAC Plugin Security: Rejected insecure file path: {$file}"); + yield $file => false; + continue; + } + $filePath = HVAC_PLUGIN_DIR . $file; - - if (file_exists($filePath)) { - require_once $filePath; + + // SECURITY FIX: Ensure file is within plugin directory + $realPath = realpath($filePath); + $pluginRealPath = realpath(HVAC_PLUGIN_DIR); + + if ($realPath === false || strpos($realPath, $pluginRealPath) !== 0) { + error_log("HVAC Plugin Security: File path outside plugin directory: {$file}"); + yield $file => false; + continue; + } + + if (file_exists($realPath) && is_readable($realPath)) { + require_once $realPath; yield $file => true; } else { yield $file => false; @@ -1400,18 +1443,35 @@ final class HVAC_Plugin { } /** - * Load feature files with memory-efficient generator - * + * Load feature files with memory-efficient generator and security validation + * * @param string[] $files Array of file paths * @return Generator File path => load status */ private function loadFeatureFiles(array $files): Generator { foreach ($files as $file) { + // SECURITY FIX: Validate file path to prevent directory traversal + if (!$this->isSecureFilePath($file)) { + error_log("HVAC Plugin Security: Rejected insecure feature file path: {$file}"); + yield $file => 'security_violation'; + continue; + } + $filePath = HVAC_PLUGIN_DIR . 'includes/' . $file; - - if (file_exists($filePath)) { + + // SECURITY FIX: Ensure file is within includes directory + $realPath = realpath($filePath); + $includesRealPath = realpath(HVAC_PLUGIN_DIR . 'includes/'); + + if ($realPath === false || strpos($realPath, $includesRealPath) !== 0) { + error_log("HVAC Plugin Security: Feature file path outside includes directory: {$file}"); + yield $file => 'security_violation'; + continue; + } + + if (file_exists($realPath) && is_readable($realPath)) { try { - require_once $filePath; + require_once $realPath; yield $file => 'loaded'; } catch (Exception $e) { error_log("Failed to load feature file {$file}: " . $e->getMessage()); @@ -1449,6 +1509,54 @@ final class HVAC_Plugin { } } + /** + * SECURITY FIX: Validate file path for security + * + * @param string $file File path to validate + * @return bool True if path is secure, false otherwise + */ + private function isSecureFilePath(string $file): bool { + // Reject paths with directory traversal attempts + if (strpos($file, '..') !== false) { + return false; + } + + // Reject absolute paths + if (strpos($file, '/') === 0 || strpos($file, '\\') === 0) { + return false; + } + + // Reject paths with null bytes (PHP injection) + if (strpos($file, "\0") !== false) { + return false; + } + + // Only allow PHP files + if (!str_ends_with(strtolower($file), '.php')) { + return false; + } + + // Reject common attack patterns + $dangerousPatterns = [ + 'php://', + 'data://', + 'http://', + 'https://', + 'ftp://', + 'phar://', + 'expect://', + 'zip://' + ]; + + foreach ($dangerousPatterns as $pattern) { + if (stripos($file, $pattern) !== false) { + return false; + } + } + + return true; + } + /** * Get component initialization status *