- Fix production debug exposure in Zoho admin interface (WP_DEBUG conditional) - Implement secure credential storage with AES-256-CBC encryption - Add file upload size limits (5MB profiles, 2MB logos) with enhanced validation - Fix privilege escalation via PHP Reflection bypass with public method alternative - Add comprehensive input validation and security headers - Update plugin version to 1.0.7 with security hardening Security improvements: ✅ Debug information exposure eliminated in production ✅ API credentials now encrypted in database storage ✅ File upload security enhanced with size/type validation ✅ AJAX endpoints secured with proper capability checks ✅ SQL injection protection verified via parameterized queries ✅ CSRF protection maintained with nonce verification 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
		
			
				
	
	
		
			186 lines
		
	
	
		
			No EOL
		
	
	
		
			4.8 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			186 lines
		
	
	
		
			No EOL
		
	
	
		
			4.8 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| /**
 | |
|  * HVAC Secure Storage - Encrypted credential storage
 | |
|  *
 | |
|  * @package HVAC_Community_Events
 | |
|  * @since 1.0.7
 | |
|  */
 | |
| 
 | |
| if (!defined('ABSPATH')) {
 | |
|     exit;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * HVAC_Secure_Storage class
 | |
|  * 
 | |
|  * Provides encrypted storage for sensitive data like API keys
 | |
|  */
 | |
| class HVAC_Secure_Storage {
 | |
|     
 | |
|     /**
 | |
|      * Encryption method
 | |
|      */
 | |
|     const ENCRYPTION_METHOD = 'AES-256-CBC';
 | |
|     
 | |
|     /**
 | |
|      * Get encryption key
 | |
|      * 
 | |
|      * @return string
 | |
|      */
 | |
|     private static function get_encryption_key() {
 | |
|         $key = get_option('hvac_encryption_key');
 | |
|         
 | |
|         if (!$key) {
 | |
|             // Generate a new key if one doesn't exist
 | |
|             $key = base64_encode(random_bytes(32));
 | |
|             update_option('hvac_encryption_key', $key);
 | |
|         }
 | |
|         
 | |
|         return base64_decode($key);
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Encrypt data
 | |
|      * 
 | |
|      * @param string $data Data to encrypt
 | |
|      * @return string Encrypted data
 | |
|      */
 | |
|     public static function encrypt($data) {
 | |
|         if (empty($data)) {
 | |
|             return '';
 | |
|         }
 | |
|         
 | |
|         $key = self::get_encryption_key();
 | |
|         $iv = random_bytes(openssl_cipher_iv_length(self::ENCRYPTION_METHOD));
 | |
|         
 | |
|         $encrypted = openssl_encrypt($data, self::ENCRYPTION_METHOD, $key, 0, $iv);
 | |
|         
 | |
|         if ($encrypted === false) {
 | |
|             return false;
 | |
|         }
 | |
|         
 | |
|         return base64_encode($iv . $encrypted);
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Decrypt data
 | |
|      * 
 | |
|      * @param string $encrypted_data Encrypted data
 | |
|      * @return string|false Decrypted data or false on failure
 | |
|      */
 | |
|     public static function decrypt($encrypted_data) {
 | |
|         if (empty($encrypted_data)) {
 | |
|             return '';
 | |
|         }
 | |
|         
 | |
|         $data = base64_decode($encrypted_data);
 | |
|         if ($data === false) {
 | |
|             return false;
 | |
|         }
 | |
|         
 | |
|         $key = self::get_encryption_key();
 | |
|         $iv_length = openssl_cipher_iv_length(self::ENCRYPTION_METHOD);
 | |
|         
 | |
|         if (strlen($data) < $iv_length) {
 | |
|             return false;
 | |
|         }
 | |
|         
 | |
|         $iv = substr($data, 0, $iv_length);
 | |
|         $encrypted = substr($data, $iv_length);
 | |
|         
 | |
|         return openssl_decrypt($encrypted, self::ENCRYPTION_METHOD, $key, 0, $iv);
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Store encrypted credential
 | |
|      * 
 | |
|      * @param string $option_name Option name
 | |
|      * @param string $value Value to store
 | |
|      * @return bool Success
 | |
|      */
 | |
|     public static function store_credential($option_name, $value) {
 | |
|         if (empty($value)) {
 | |
|             delete_option($option_name);
 | |
|             return true;
 | |
|         }
 | |
|         
 | |
|         $encrypted = self::encrypt($value);
 | |
|         if ($encrypted === false) {
 | |
|             return false;
 | |
|         }
 | |
|         
 | |
|         return update_option($option_name, $encrypted);
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Retrieve encrypted credential
 | |
|      * 
 | |
|      * @param string $option_name Option name
 | |
|      * @param string $default Default value
 | |
|      * @return string Decrypted value
 | |
|      */
 | |
|     public static function get_credential($option_name, $default = '') {
 | |
|         $encrypted = get_option($option_name, '');
 | |
|         
 | |
|         if (empty($encrypted)) {
 | |
|             return $default;
 | |
|         }
 | |
|         
 | |
|         $decrypted = self::decrypt($encrypted);
 | |
|         return $decrypted !== false ? $decrypted : $default;
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Migrate existing plaintext credentials to encrypted storage
 | |
|      * 
 | |
|      * @return array Migration results
 | |
|      */
 | |
|     public static function migrate_existing_credentials() {
 | |
|         $credentials_to_migrate = [
 | |
|             'hvac_zoho_client_id',
 | |
|             'hvac_zoho_client_secret',
 | |
|             'hvac_zoho_refresh_token',
 | |
|             'hvac_google_maps_api_key'
 | |
|         ];
 | |
|         
 | |
|         $results = [
 | |
|             'migrated' => 0,
 | |
|             'skipped' => 0,
 | |
|             'errors' => []
 | |
|         ];
 | |
|         
 | |
|         foreach ($credentials_to_migrate as $option_name) {
 | |
|             $plaintext = get_option($option_name, '');
 | |
|             
 | |
|             if (empty($plaintext)) {
 | |
|                 $results['skipped']++;
 | |
|                 continue;
 | |
|             }
 | |
|             
 | |
|             // Check if it's already encrypted (basic heuristic)
 | |
|             $decrypted = self::decrypt($plaintext);
 | |
|             if ($decrypted !== false && $decrypted !== $plaintext) {
 | |
|                 $results['skipped']++;
 | |
|                 continue;
 | |
|             }
 | |
|             
 | |
|             // Migrate to encrypted storage
 | |
|             if (self::store_credential($option_name, $plaintext)) {
 | |
|                 $results['migrated']++;
 | |
|             } else {
 | |
|                 $results['errors'][] = "Failed to migrate: $option_name";
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         return $results;
 | |
|     }
 | |
|     
 | |
|     /**
 | |
|      * Check if OpenSSL is available
 | |
|      * 
 | |
|      * @return bool
 | |
|      */
 | |
|     public static function is_encryption_available() {
 | |
|         return function_exists('openssl_encrypt') && function_exists('openssl_decrypt');
 | |
|     }
 | |
| } |