init_hooks(); } /** * Initialize hooks */ private function init_hooks() { // Add security headers add_action('admin_init', array($this, 'add_security_headers')); // Sanitize and validate all inputs add_filter('aha_mail_smtp_sanitize_setting', array($this, 'sanitize_setting'), 10, 3); // Encrypt sensitive data add_filter('pre_update_option_aha_mail_smtp_smtp_password', array($this, 'encrypt_password')); add_filter('option_aha_mail_smtp_smtp_password', array($this, 'decrypt_password')); // Rate limiting for AJAX requests add_action('wp_ajax_aha_mail_smtp_send_test', array($this, 'rate_limit_test_email'), 1); // Log security events add_action('aha_mail_smtp_security_event', array($this, 'log_security_event'), 10, 3); } /** * Add security headers */ public function add_security_headers() { if (!$this->is_plugin_page()) { return; } // Prevent clickjacking header('X-Frame-Options: SAMEORIGIN'); // Prevent MIME type sniffing header('X-Content-Type-Options: nosniff'); // XSS protection header('X-XSS-Protection: 1; mode=block'); // Referrer policy header('Referrer-Policy: strict-origin-when-cross-origin'); } /** * Check if current page is plugin page */ private function is_plugin_page() { return isset($_GET['page']) && $_GET['page'] === 'aha-mail-smtp'; } /** * Sanitize setting based on type */ public function sanitize_setting($value, $key, $type = 'text') { switch ($type) { case 'email': return sanitize_email($value); case 'url': return esc_url_raw($value); case 'int': return (int) $value; case 'bool': return (bool) $value; case 'textarea': return sanitize_textarea_field($value); case 'password': // Don't sanitize passwords as it might break them return $value; case 'text': default: return sanitize_text_field($value); } } /** * Encrypt password before storing */ public function encrypt_password($value) { if (empty($value)) { return $value; } // Use WordPress built-in encryption if available if (function_exists('wp_hash_password')) { // For SMTP passwords, we need reversible encryption, not hashing return $this->encrypt_data($value); } return $value; } /** * Decrypt password when retrieving */ public function decrypt_password($value) { if (empty($value)) { return $value; } // Check if value is encrypted if ($this->is_encrypted($value)) { return $this->decrypt_data($value); } return $value; } /** * Encrypt sensitive data */ /** * Encryption prefix to distinguish encrypted values from plain text */ const ENCRYPTION_PREFIX = 'AHA_ENC:'; private function encrypt_data($data) { if (empty($data)) { return $data; } $key = $this->get_encryption_key(); $iv = openssl_random_pseudo_bytes(16); $encrypted = openssl_encrypt($data, 'AES-256-CBC', $key, 0, $iv); return self::ENCRYPTION_PREFIX . base64_encode($iv . $encrypted); } /** * Decrypt sensitive data */ private function decrypt_data($encrypted_data) { if (empty($encrypted_data)) { return $encrypted_data; } $key = $this->get_encryption_key(); $raw = base64_decode(substr($encrypted_data, strlen(self::ENCRYPTION_PREFIX))); $iv = substr($raw, 0, 16); $encrypted = substr($raw, 16); return openssl_decrypt($encrypted, 'AES-256-CBC', $key, 0, $iv); } /** * Check if data is encrypted (uses prefix marker to avoid false positives) */ private function is_encrypted($data) { return strpos($data, self::ENCRYPTION_PREFIX) === 0; } /** * Get encryption key */ private function get_encryption_key() { // Use WordPress salts for encryption key $key = SECURE_AUTH_KEY . LOGGED_IN_KEY; return hash('sha256', $key); } /** * Verify nonce for AJAX requests */ public static function verify_ajax_nonce($action = 'aha_mail_smtp_admin') { if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), $action)) { wp_send_json_error(__('Security check failed', 'aha-mail-smtp')); exit; } } /** * Check user capabilities */ public static function check_user_capabilities($capability = 'manage_options') { if (!current_user_can($capability)) { wp_send_json_error(__('You do not have sufficient permissions', 'aha-mail-smtp')); exit; } } /** * Rate limit test email requests */ public function rate_limit_test_email() { $user_id = get_current_user_id(); $rate_limit_key = 'aha_mail_smtp_test_email_' . $user_id; $attempts = get_transient($rate_limit_key); if ($attempts === false) { $attempts = 0; } // Allow 5 test emails per hour if ($attempts >= 5) { wp_send_json_error(__('Rate limit exceeded. Please wait before sending another test email.', 'aha-mail-smtp')); exit; } // Increment attempts set_transient($rate_limit_key, $attempts + 1, HOUR_IN_SECONDS); } /** * Validate SMTP settings */ public static function validate_smtp_settings($settings) { $errors = array(); // Validate host if (empty($settings['smtp_host'])) { $errors[] = __('SMTP host is required', 'aha-mail-smtp'); } elseif (!self::is_valid_hostname($settings['smtp_host'])) { $errors[] = __('Invalid SMTP host format', 'aha-mail-smtp'); } // Validate port if (empty($settings['smtp_port']) || !is_numeric($settings['smtp_port'])) { $errors[] = __('Valid SMTP port is required', 'aha-mail-smtp'); } elseif ($settings['smtp_port'] < 1 || $settings['smtp_port'] > 65535) { $errors[] = __('SMTP port must be between 1 and 65535', 'aha-mail-smtp'); } // Validate encryption $valid_encryptions = array('none', 'ssl', 'tls'); if (!in_array($settings['smtp_encryption'], $valid_encryptions)) { $errors[] = __('Invalid encryption method', 'aha-mail-smtp'); } // Validate from email if (!empty($settings['from_email']) && !is_email($settings['from_email'])) { $errors[] = __('Invalid from email address', 'aha-mail-smtp'); } // Validate reply-to email if (!empty($settings['reply_to_email']) && !is_email($settings['reply_to_email'])) { $errors[] = __('Invalid reply-to email address', 'aha-mail-smtp'); } // Validate return path if (!empty($settings['return_path']) && !is_email($settings['return_path'])) { $errors[] = __('Invalid return path email address', 'aha-mail-smtp'); } return $errors; } /** * Validate hostname */ private static function is_valid_hostname($hostname) { return filter_var($hostname, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) !== false; } /** * Sanitize file upload */ public static function sanitize_file_upload($file) { // Check file type $allowed_types = array('json'); $file_type = pathinfo($file['name'], PATHINFO_EXTENSION); if (!in_array(strtolower($file_type), $allowed_types)) { return new WP_Error('invalid_file_type', __('Only JSON files are allowed', 'aha-mail-smtp')); } // Check file size (max 1MB) if ($file['size'] > 1048576) { return new WP_Error('file_too_large', __('File size must be less than 1MB', 'aha-mail-smtp')); } // Validate JSON content $content = file_get_contents($file['tmp_name']); $json_data = json_decode($content, true); if (json_last_error() !== JSON_ERROR_NONE) { return new WP_Error('invalid_json', __('Invalid JSON file format', 'aha-mail-smtp')); } return $json_data; } /** * Log security events */ public function log_security_event($event_type, $description, $user_id = null) { if (!$user_id) { $user_id = get_current_user_id(); } $log_entry = array( 'timestamp' => current_time('mysql'), 'event_type' => $event_type, 'description' => $description, 'user_id' => $user_id, 'ip_address' => $this->get_client_ip(), 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '' ); // Store in WordPress options with autoload disabled to avoid performance impact $security_logs = get_option('aha_mail_smtp_security_logs', array()); if (!is_array($security_logs)) { $security_logs = array(); } $security_logs[] = $log_entry; // Keep only last 100 entries if (count($security_logs) > 100) { $security_logs = array_slice($security_logs, -100); } // Use 'no' autoload to prevent loading on every page request if (false === get_option('aha_mail_smtp_security_logs')) { add_option('aha_mail_smtp_security_logs', $security_logs, '', 'no'); } else { update_option('aha_mail_smtp_security_logs', $security_logs, 'no'); } } /** * Get client IP address */ private function get_client_ip() { $ip_keys = array( 'HTTP_CF_CONNECTING_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) { $ip = $_SERVER[$key]; if (strpos($ip, ',') !== false) { $ip = explode(',', $ip)[0]; } $ip = trim($ip); if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { return $ip; } } } return isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : 'unknown'; } /** * Generate secure random token */ public static function generate_secure_token($length = 32) { return bin2hex(random_bytes($length / 2)); } /** * Hash sensitive data */ public static function hash_data($data, $salt = '') { if (empty($salt)) { $salt = wp_salt(); } return hash_hmac('sha256', $data, $salt); } /** * Verify hashed data */ public static function verify_hash($data, $hash, $salt = '') { if (empty($salt)) { $salt = wp_salt(); } return hash_equals($hash, self::hash_data($data, $salt)); } /** * Clean up expired security logs */ public static function cleanup_security_logs() { $security_logs = get_option('aha_mail_smtp_security_logs', array()); $cutoff_time = strtotime('-30 days'); $cleaned_logs = array_filter($security_logs, function($log) use ($cutoff_time) { return strtotime($log['timestamp']) > $cutoff_time; }); update_option('aha_mail_smtp_security_logs', array_values($cleaned_logs)); } /** * Check for suspicious activity */ public static function check_suspicious_activity() { $security_logs = get_option('aha_mail_smtp_security_logs', array()); $recent_logs = array_filter($security_logs, function($log) { return strtotime($log['timestamp']) > strtotime('-1 hour'); }); // Check for too many failed attempts $failed_attempts = array_filter($recent_logs, function($log) { return $log['event_type'] === 'failed_login' || $log['event_type'] === 'invalid_nonce'; }); if (count($failed_attempts) > 10) { do_action('aha_mail_smtp_security_alert', 'suspicious_activity', $failed_attempts); return true; } return false; } /** * Escape output for different contexts */ public static function escape_output($data, $context = 'html') { switch ($context) { case 'attr': return esc_attr($data); case 'url': return esc_url($data); case 'js': return esc_js($data); case 'textarea': return esc_textarea($data); case 'html': default: return esc_html($data); } } /** * Validate and sanitize import data */ public static function validate_import_data($data) { if (!is_array($data)) { return new WP_Error('invalid_format', __('Invalid import data format', 'aha-mail-smtp')); } $allowed_settings = array( 'smtp_host', 'smtp_port', 'smtp_encryption', 'smtp_auth', 'smtp_username', 'from_email', 'from_name', 'force_from', 'force_from_email', 'force_from_name', 'reply_to_email', 'return_path', 'enable_logging', 'debug_mode', 'provider', 'connection_mode', 'api_key', 'api_domain', 'api_region', 'client_id' ); // Use the input directly if it looks like a flat settings array, // or check for a nested 'settings' key for forward-compatibility. $raw_settings = isset($data['settings']) && is_array($data['settings']) ? $data['settings'] : $data; $sanitized_settings = array(); foreach ($raw_settings as $key => $value) { if (in_array($key, $allowed_settings, true)) { // Sanitize using the Settings class save logic $sanitized_settings[$key] = $value; } } if (empty($sanitized_settings)) { return new WP_Error('no_valid_settings', __('No valid settings found in import data', 'aha-mail-smtp')); } return $sanitized_settings; } }