init_hooks(); } /** * Initialize security hooks */ private function init_hooks() { // Add security headers add_action('send_headers', array($this, 'send_security_headers')); // AJAX security middleware add_action('wp_ajax_nopriv_hvac_security_check', array($this, 'block_nopriv_ajax')); // Initialize audit logging add_action('init', array($this, 'init_audit_logging')); } /** * Send security headers */ public function send_security_headers() { if (!is_admin() && wp_doing_ajax()) { // Content Security Policy header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';"); // Additional security headers header('X-Content-Type-Options: nosniff'); header('X-Frame-Options: SAMEORIGIN'); header('X-XSS-Protection: 1; mode=block'); header('Referrer-Policy: strict-origin-when-cross-origin'); // CORS configuration (adjust origin as needed) $allowed_origin = get_site_url(); header("Access-Control-Allow-Origin: $allowed_origin"); header('Access-Control-Allow-Credentials: true'); header('Access-Control-Allow-Methods: POST, GET, OPTIONS'); header('Access-Control-Max-Age: 86400'); } } /** * Verify AJAX request security * * @param string $action AJAX action being performed * @param string $nonce_action Nonce action to verify * @param array $capabilities Required capabilities * @param bool $is_sensitive Whether this is a sensitive action * @return bool|WP_Error True if valid, WP_Error on failure */ public static function verify_ajax_request($action, $nonce_action = null, $capabilities = array(), $is_sensitive = false) { $security = self::get_instance(); // 1. Check if user is logged in if (!is_user_logged_in()) { $security->log_security_event('ajax_unauthorized', $action); return new WP_Error('unauthorized', 'Authentication required', array('status' => 401)); } // 2. Verify nonce $nonce = isset($_REQUEST['nonce']) ? sanitize_text_field($_REQUEST['nonce']) : ''; $nonce_action = $nonce_action ?: self::NONCE_GENERAL; if (!wp_verify_nonce($nonce, $nonce_action)) { $security->log_security_event('ajax_invalid_nonce', $action); return new WP_Error('invalid_nonce', 'Security token expired or invalid', array('status' => 403)); } // 3. Check rate limiting $rate_limit = $security->check_rate_limit($action, $is_sensitive); if (is_wp_error($rate_limit)) { return $rate_limit; } // 4. Verify capabilities if (!empty($capabilities)) { $has_cap = false; foreach ($capabilities as $capability) { if (current_user_can($capability)) { $has_cap = true; break; } } if (!$has_cap) { $security->log_security_event('ajax_insufficient_permissions', $action); return new WP_Error('insufficient_permissions', 'You do not have permission to perform this action', array('status' => 403)); } } // 5. Log successful authentication for sensitive actions if ($is_sensitive) { $security->log_security_event('ajax_sensitive_action', $action, 'info'); } return true; } /** * Check rate limiting * * @param string $action Action being performed * @param bool $is_sensitive Whether this is a sensitive action * @return bool|WP_Error True if within limits, WP_Error if exceeded */ private function check_rate_limit($action, $is_sensitive = false) { $user_id = get_current_user_id(); $ip_address = $this->get_client_ip(); // Use both user ID and IP for rate limiting $rate_key = 'hvac_rate_' . md5($user_id . '_' . $ip_address . '_' . $action); $sensitive_key = 'hvac_sensitive_' . md5($user_id . '_' . $ip_address); // Check general rate limit $requests = get_transient($rate_key); $limit = $is_sensitive ? self::RATE_LIMIT_SENSITIVE : self::RATE_LIMIT_REQUESTS; if (false === $requests) { $requests = 0; } if ($requests >= $limit) { $this->log_security_event('ajax_rate_limit_exceeded', $action, 'warning'); return new WP_Error('rate_limit_exceeded', 'Too many requests. Please wait and try again.', array('status' => 429)); } // Increment counter set_transient($rate_key, $requests + 1, self::RATE_LIMIT_WINDOW); // Additional check for sensitive actions across all endpoints if ($is_sensitive) { $sensitive_count = get_transient($sensitive_key); if (false === $sensitive_count) { $sensitive_count = 0; } if ($sensitive_count >= self::RATE_LIMIT_SENSITIVE) { $this->log_security_event('ajax_sensitive_rate_limit_exceeded', $action, 'critical'); return new WP_Error('rate_limit_exceeded', 'Too many sensitive actions. Please wait and try again.', array('status' => 429)); } set_transient($sensitive_key, $sensitive_count + 1, self::RATE_LIMIT_WINDOW * 5); // 5 minute window for sensitive } return true; } /** * Sanitize and validate input data * * @param array $data Input data * @param array $rules Validation rules * @return array|WP_Error Sanitized data or error */ public static function sanitize_input($data, $rules) { $sanitized = array(); $errors = array(); foreach ($rules as $field => $rule) { $value = isset($data[$field]) ? $data[$field] : null; // Check required fields if (isset($rule['required']) && $rule['required'] && empty($value)) { $errors[] = sprintf('Field "%s" is required', $field); continue; } // Skip optional empty fields if (empty($value) && (!isset($rule['required']) || !$rule['required'])) { continue; } // Apply sanitization based on type $type = isset($rule['type']) ? $rule['type'] : 'text'; switch ($type) { case 'int': $sanitized[$field] = intval($value); if (isset($rule['min']) && $sanitized[$field] < $rule['min']) { $errors[] = sprintf('Field "%s" must be at least %d', $field, $rule['min']); } if (isset($rule['max']) && $sanitized[$field] > $rule['max']) { $errors[] = sprintf('Field "%s" must be at most %d', $field, $rule['max']); } break; case 'email': $sanitized[$field] = sanitize_email($value); if (!is_email($sanitized[$field])) { $errors[] = sprintf('Field "%s" must be a valid email', $field); } break; case 'url': $sanitized[$field] = esc_url_raw($value); if (!filter_var($sanitized[$field], FILTER_VALIDATE_URL)) { $errors[] = sprintf('Field "%s" must be a valid URL', $field); } break; case 'textarea': $sanitized[$field] = sanitize_textarea_field($value); if (isset($rule['max_length']) && strlen($sanitized[$field]) > $rule['max_length']) { $errors[] = sprintf('Field "%s" must be at most %d characters', $field, $rule['max_length']); } break; case 'html': $allowed_html = isset($rule['allowed_html']) ? $rule['allowed_html'] : wp_kses_allowed_html('post'); $sanitized[$field] = wp_kses($value, $allowed_html); break; case 'boolean': $sanitized[$field] = filter_var($value, FILTER_VALIDATE_BOOLEAN); break; case 'array': if (!is_array($value)) { $errors[] = sprintf('Field "%s" must be an array', $field); } else { $sanitized[$field] = array_map('sanitize_text_field', $value); } break; default: $sanitized[$field] = sanitize_text_field($value); if (isset($rule['max_length']) && strlen($sanitized[$field]) > $rule['max_length']) { $errors[] = sprintf('Field "%s" must be at most %d characters', $field, $rule['max_length']); } } // Custom validation callback if (isset($rule['validate']) && is_callable($rule['validate'])) { $validation_result = call_user_func($rule['validate'], $sanitized[$field]); if (is_wp_error($validation_result)) { $errors[] = $validation_result->get_error_message(); } } } if (!empty($errors)) { return new WP_Error('validation_failed', 'Input validation failed', array('errors' => $errors)); } return $sanitized; } /** * Get client IP address * * @return string */ private function get_client_ip() { $ip_keys = array('HTTP_CF_CONNECTING_IP', 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR'); foreach ($ip_keys as $key) { if (array_key_exists($key, $_SERVER) === true) { foreach (explode(',', $_SERVER[$key]) as $ip) { $ip = trim($ip); if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) { return $ip; } } } } return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0'; } /** * Log security events * * @param string $event_type Type of security event * @param string $context Additional context * @param string $level Log level (info, warning, critical) */ private function log_security_event($event_type, $context = '', $level = 'warning') { $user_id = get_current_user_id(); $ip_address = $this->get_client_ip(); $log_entry = array( 'timestamp' => current_time('mysql'), 'event_type' => $event_type, 'user_id' => $user_id, 'ip_address' => $ip_address, 'context' => $context, 'level' => $level, 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '', 'request_uri' => isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '' ); // Use WordPress logging if available if (class_exists('HVAC_Logger')) { HVAC_Logger::log($level, 'Security: ' . $event_type, $log_entry); } // Store in database for audit trail $this->store_audit_log($log_entry); // For critical events, notify admin if ($level === 'critical') { $this->notify_admin_security_event($log_entry); } } /** * Store audit log in database * * @param array $log_entry Log entry data */ private function store_audit_log($log_entry) { global $wpdb; $table_name = $wpdb->prefix . 'hvac_security_audit'; // Create table if not exists if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") != $table_name) { $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE $table_name ( id bigint(20) NOT NULL AUTO_INCREMENT, timestamp datetime DEFAULT CURRENT_TIMESTAMP, event_type varchar(100) NOT NULL, user_id bigint(20), ip_address varchar(45), context text, level varchar(20), user_agent text, request_uri text, PRIMARY KEY (id), KEY event_type (event_type), KEY user_id (user_id), KEY timestamp (timestamp) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } // Insert log entry $wpdb->insert( $table_name, $log_entry, array('%s', '%s', '%d', '%s', '%s', '%s', '%s', '%s') ); } /** * Initialize audit logging system */ public function init_audit_logging() { // Clean old audit logs (keep 30 days) if (!wp_next_scheduled('hvac_clean_audit_logs')) { wp_schedule_event(time(), 'daily', 'hvac_clean_audit_logs'); } add_action('hvac_clean_audit_logs', array($this, 'clean_old_audit_logs')); } /** * Clean old audit logs */ public function clean_old_audit_logs() { global $wpdb; $table_name = $wpdb->prefix . 'hvac_security_audit'; $wpdb->query($wpdb->prepare( "DELETE FROM $table_name WHERE timestamp < DATE_SUB(NOW(), INTERVAL %d DAY)", 30 )); } /** * Notify admin of critical security events * * @param array $log_entry Log entry data */ private function notify_admin_security_event($log_entry) { $admin_email = get_option('admin_email'); $site_name = get_bloginfo('name'); $subject = sprintf('[%s] Critical Security Event: %s', $site_name, $log_entry['event_type']); $message = sprintf( "A critical security event has been detected on your site.\n\n" . "Event Type: %s\n" . "Time: %s\n" . "User ID: %s\n" . "IP Address: %s\n" . "Context: %s\n" . "User Agent: %s\n" . "Request URI: %s\n\n" . "Please review your security logs for more information.", $log_entry['event_type'], $log_entry['timestamp'], $log_entry['user_id'], $log_entry['ip_address'], $log_entry['context'], $log_entry['user_agent'], $log_entry['request_uri'] ); wp_mail($admin_email, $subject, $message); } /** * Block non-privileged AJAX requests */ public function block_nopriv_ajax() { wp_send_json_error('Unauthorized access', 401); } /** * Generate secure token for sensitive operations * * @param string $action Action identifier * @param int $user_id User ID * @return string Secure token */ public static function generate_secure_token($action, $user_id) { $salt = wp_salt('auth'); $data = $action . '|' . $user_id . '|' . time(); return hash_hmac('sha256', $data, $salt); } /** * Verify secure token * * @param string $token Token to verify * @param string $action Action identifier * @param int $user_id User ID * @param int $expiry Token expiry in seconds (default 1 hour) * @return bool */ public static function verify_secure_token($token, $action, $user_id, $expiry = 3600) { $salt = wp_salt('auth'); // Generate tokens for last $expiry seconds $current_time = time(); for ($i = 0; $i <= $expiry; $i++) { $data = $action . '|' . $user_id . '|' . ($current_time - $i); $expected_token = hash_hmac('sha256', $data, $salt); if (hash_equals($expected_token, $token)) { return true; } } return false; } } // Initialize the security handler HVAC_Ajax_Security::get_instance();