← All changes
|
includes/widgets/Login_Register_Form/Security_Manager.php
+382
-0
51.1.2
→
51.1.86
View file →
| @@ -1,0 +1,382 @@ | ||
| 1 | +<?php | |
| 2 | + | |
| 3 | +namespace King_Addons\Widgets\Login_Register_Form; | |
| 4 | + | |
| 5 | +if (!defined('ABSPATH')) { | |
| 6 | + exit; // Exit if accessed directly. | |
| 7 | +} | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * Security Manager for Login Register Form widget | |
| 11 | + * Handles rate limiting, file validation, and other security measures | |
| 12 | + */ | |
| 13 | +class Security_Manager | |
| 14 | +{ | |
| 15 | + /** | |
| 16 | + * Rate limiting settings | |
| 17 | + */ | |
| 18 | + const MAX_LOGIN_ATTEMPTS = 5; | |
| 19 | + const MAX_REGISTER_ATTEMPTS = 3; | |
| 20 | + const MAX_LOST_PASSWORD_ATTEMPTS = 3; | |
| 21 | + const LOCKOUT_DURATION = 900; // 15 minutes in seconds | |
| 22 | + const ALLOWED_FILE_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'text/plain']; | |
| 23 | + const MAX_FILE_SIZE = 5242880; // 5MB in bytes | |
| 24 | + | |
| 25 | + /** | |
| 26 | + * Check if IP is rate limited for specific action | |
| 27 | + */ | |
| 28 | + public static function is_rate_limited($action, $ip_address = null) | |
| 29 | + { | |
| 30 | + if (!$ip_address) { | |
| 31 | + $ip_address = self::get_client_ip(); | |
| 32 | + } | |
| 33 | + | |
| 34 | + $transient_key = "king_addons_{$action}_attempts_" . md5($ip_address); | |
| 35 | + $attempts = get_transient($transient_key); | |
| 36 | + | |
| 37 | + $max_attempts = self::get_max_attempts($action); | |
| 38 | + | |
| 39 | + return $attempts !== false && $attempts >= $max_attempts; | |
| 40 | + } | |
| 41 | + | |
| 42 | + /** | |
| 43 | + * Record a failed attempt | |
| 44 | + */ | |
| 45 | + public static function record_failed_attempt($action, $ip_address = null) | |
| 46 | + { | |
| 47 | + if (!$ip_address) { | |
| 48 | + $ip_address = self::get_client_ip(); | |
| 49 | + } | |
| 50 | + | |
| 51 | + $transient_key = "king_addons_{$action}_attempts_" . md5($ip_address); | |
| 52 | + $attempts = get_transient($transient_key); | |
| 53 | + | |
| 54 | + if ($attempts === false) { | |
| 55 | + $attempts = 0; | |
| 56 | + } | |
| 57 | + | |
| 58 | + $attempts++; | |
| 59 | + set_transient($transient_key, $attempts, self::LOCKOUT_DURATION); | |
| 60 | + | |
| 61 | + // Log security event | |
| 62 | + // error_log("King Addons Security: Failed {$action} attempt #{$attempts} from IP {$ip_address}"); | |
| 63 | + | |
| 64 | + return $attempts; | |
| 65 | + } | |
| 66 | + | |
| 67 | + /** | |
| 68 | + * Clear failed attempts (on successful login/registration) | |
| 69 | + */ | |
| 70 | + public static function clear_failed_attempts($action, $ip_address = null) | |
| 71 | + { | |
| 72 | + if (!$ip_address) { | |
| 73 | + $ip_address = self::get_client_ip(); | |
| 74 | + } | |
| 75 | + | |
| 76 | + $transient_key = "king_addons_{$action}_attempts_" . md5($ip_address); | |
| 77 | + delete_transient($transient_key); | |
| 78 | + } | |
| 79 | + | |
| 80 | + /** | |
| 81 | + * Get remaining lockout time | |
| 82 | + */ | |
| 83 | + public static function get_remaining_lockout_time($action, $ip_address = null) | |
| 84 | + { | |
| 85 | + if (!$ip_address) { | |
| 86 | + $ip_address = self::get_client_ip(); | |
| 87 | + } | |
| 88 | + | |
| 89 | + $transient_key = "king_addons_{$action}_attempts_" . md5($ip_address); | |
| 90 | + $expiration = get_option('_transient_timeout_' . $transient_key); | |
| 91 | + | |
| 92 | + if ($expiration === false) { | |
| 93 | + return 0; | |
| 94 | + } | |
| 95 | + | |
| 96 | + $remaining = $expiration - time(); | |
| 97 | + return max(0, $remaining); | |
| 98 | + } | |
| 99 | + | |
| 100 | + /** | |
| 101 | + * Validate uploaded file | |
| 102 | + */ | |
| 103 | + public static function validate_file_upload($file_data) | |
| 104 | + { | |
| 105 | + // Check if file was uploaded | |
| 106 | + if (empty($file_data['name']) || empty($file_data['tmp_name'])) { | |
| 107 | + return [ | |
| 108 | + 'valid' => false, | |
| 109 | + 'error' => esc_html__('No file uploaded.', 'king-addons') | |
| 110 | + ]; | |
| 111 | + } | |
| 112 | + | |
| 113 | + // Check file size | |
| 114 | + if ($file_data['size'] > self::MAX_FILE_SIZE) { | |
| 115 | + return [ | |
| 116 | + 'valid' => false, | |
| 117 | + 'error' => sprintf( | |
| 118 | + esc_html__('File size exceeds maximum allowed size of %s.', 'king-addons'), | |
| 119 | + size_format(self::MAX_FILE_SIZE) | |
| 120 | + ) | |
| 121 | + ]; | |
| 122 | + } | |
| 123 | + | |
| 124 | + // Check MIME type | |
| 125 | + $file_type = wp_check_filetype($file_data['name']); | |
| 126 | + if (!$file_type['type'] || !in_array($file_type['type'], self::ALLOWED_FILE_TYPES)) { | |
| 127 | + return [ | |
| 128 | + 'valid' => false, | |
| 129 | + 'error' => esc_html__('File type not allowed. Please upload images (JPG, PNG, GIF), PDF, or text files only.', 'king-addons') | |
| 130 | + ]; | |
| 131 | + } | |
| 132 | + | |
| 133 | + // Additional security checks | |
| 134 | + $real_mime = mime_content_type($file_data['tmp_name']); | |
| 135 | + if ($real_mime && $real_mime !== $file_type['type']) { | |
| 136 | + return [ | |
| 137 | + 'valid' => false, | |
| 138 | + 'error' => esc_html__('File type mismatch detected. Upload rejected for security.', 'king-addons') | |
| 139 | + ]; | |
| 140 | + } | |
| 141 | + | |
| 142 | + // Check for malicious content in text files (limit file size to prevent DoS) | |
| 143 | + if (in_array($file_type['type'], ['text/plain', 'application/pdf'])) { | |
| 144 | + // Security fix: Check file size before reading to prevent DoS | |
| 145 | + $file_size = filesize($file_data['tmp_name']); | |
| 146 | + if ($file_size > 1024 * 1024) { // 1MB limit for content scanning | |
| 147 | + return [ | |
| 148 | + 'valid' => false, | |
| 149 | + 'error' => esc_html__('File too large for content scanning.', 'king-addons') | |
| 150 | + ]; | |
| 151 | + } | |
| 152 | + | |
| 153 | + $content = file_get_contents($file_data['tmp_name']); | |
| 154 | + if (self::contains_malicious_content($content)) { | |
| 155 | + return [ | |
| 156 | + 'valid' => false, | |
| 157 | + 'error' => esc_html__('File contains suspicious content and cannot be uploaded.', 'king-addons') | |
| 158 | + ]; | |
| 159 | + } | |
| 160 | + } | |
| 161 | + | |
| 162 | + return ['valid' => true]; | |
| 163 | + } | |
| 164 | + | |
| 165 | + /** | |
| 166 | + * Sanitize social login data | |
| 167 | + */ | |
| 168 | + public static function sanitize_social_data($data, $provider) | |
| 169 | + { | |
| 170 | + $sanitized = []; | |
| 171 | + | |
| 172 | + // Basic required fields | |
| 173 | + $sanitized['email'] = isset($data['email']) ? sanitize_email($data['email']) : ''; | |
| 174 | + $sanitized['name'] = isset($data['name']) ? sanitize_text_field($data['name']) : ''; | |
| 175 | + $sanitized['provider_id'] = isset($data['id']) ? sanitize_text_field($data['id']) : ''; | |
| 176 | + | |
| 177 | + // Optional fields | |
| 178 | + $sanitized['first_name'] = isset($data['given_name']) ? sanitize_text_field($data['given_name']) : ''; | |
| 179 | + $sanitized['last_name'] = isset($data['family_name']) ? sanitize_text_field($data['family_name']) : ''; | |
| 180 | + | |
| 181 | + // Picture URL with strict validation | |
| 182 | + if (isset($data['picture'])) { | |
| 183 | + $picture_url = esc_url_raw($data['picture']); | |
| 184 | + // Additional validation for picture URL | |
| 185 | + if (filter_var($picture_url, FILTER_VALIDATE_URL) && self::is_safe_image_url($picture_url)) { | |
| 186 | + $sanitized['picture'] = $picture_url; | |
| 187 | + } else { | |
| 188 | + $sanitized['picture'] = ''; | |
| 189 | + } | |
| 190 | + } else { | |
| 191 | + $sanitized['picture'] = ''; | |
| 192 | + } | |
| 193 | + | |
| 194 | + // Validate email domain for additional security | |
| 195 | + if (!empty($sanitized['email']) && !self::is_safe_email_domain($sanitized['email'])) { | |
| 196 | + // error_log("King Addons Security: Suspicious email domain from {$provider}: {$sanitized['email']}"); | |
| 197 | + } | |
| 198 | + | |
| 199 | + return $sanitized; | |
| 200 | + } | |
| 201 | + | |
| 202 | + /** | |
| 203 | + * Check for suspicious patterns in registration data | |
| 204 | + */ | |
| 205 | + public static function detect_suspicious_registration($data) | |
| 206 | + { | |
| 207 | + $suspicious_patterns = [ | |
| 208 | + // Common spam patterns | |
| 209 | + '/\b(viagra|cialis|casino|poker|lottery|winner|congratulations)\b/i', | |
| 210 | + // Suspicious email patterns | |
| 211 | + '/\b\d{10,}@/', // Long numeric sequences in email | |
| 212 | + // Bot-like usernames | |
| 213 | + '/^(user|test|admin)\d+$/i', | |
| 214 | + ]; | |
| 215 | + | |
| 216 | + $text_to_check = implode(' ', [ | |
| 217 | + $data['username'] ?? '', | |
| 218 | + $data['email'] ?? '', | |
| 219 | + $data['first_name'] ?? '', | |
| 220 | + $data['last_name'] ?? '' | |
| 221 | + ]); | |
| 222 | + | |
| 223 | + foreach ($suspicious_patterns as $pattern) { | |
| 224 | + if (preg_match($pattern, $text_to_check)) { | |
| 225 | + // error_log("King Addons Security: Suspicious registration pattern detected: {$pattern}"); | |
| 226 | + return true; | |
| 227 | + } | |
| 228 | + } | |
| 229 | + | |
| 230 | + return false; | |
| 231 | + } | |
| 232 | + | |
| 233 | + /** | |
| 234 | + * Enhanced password strength validation | |
| 235 | + */ | |
| 236 | + public static function validate_password_strength($password) | |
| 237 | + { | |
| 238 | + $strength = [ | |
| 239 | + 'score' => 0, | |
| 240 | + 'feedback' => [], | |
| 241 | + 'valid' => true | |
| 242 | + ]; | |
| 243 | + | |
| 244 | + // Basic length check | |
| 245 | + if (strlen($password) < 8) { | |
| 246 | + $strength['valid'] = false; | |
| 247 | + $strength['feedback'][] = esc_html__('Password must be at least 8 characters long.', 'king-addons'); | |
| 248 | + return $strength; | |
| 249 | + } | |
| 250 | + | |
| 251 | + // Check for character variety | |
| 252 | + $patterns = [ | |
| 253 | + 'lowercase' => '/[a-z]/', | |
| 254 | + 'uppercase' => '/[A-Z]/', | |
| 255 | + 'numbers' => '/\d/', | |
| 256 | + 'special' => '/[!@#$%^&*(),.?":{}|<>]/' | |
| 257 | + ]; | |
| 258 | + | |
| 259 | + foreach ($patterns as $type => $pattern) { | |
| 260 | + if (preg_match($pattern, $password)) { | |
| 261 | + $strength['score']++; | |
| 262 | + } | |
| 263 | + } | |
| 264 | + | |
| 265 | + // Check against common passwords | |
| 266 | + if (self::is_common_password($password)) { | |
| 267 | + $strength['valid'] = false; | |
| 268 | + $strength['feedback'][] = esc_html__('This password is too common. Please choose a more unique password.', 'king-addons'); | |
| 269 | + } | |
| 270 | + | |
| 271 | + // Length bonus | |
| 272 | + if (strlen($password) >= 12) { | |
| 273 | + $strength['score']++; | |
| 274 | + } | |
| 275 | + | |
| 276 | + // Determine if password is strong enough | |
| 277 | + if ($strength['score'] < 3) { | |
| 278 | + $strength['valid'] = false; | |
| 279 | + $strength['feedback'][] = esc_html__('Password should contain a mix of uppercase, lowercase, numbers, and special characters.', 'king-addons'); | |
| 280 | + } | |
| 281 | + | |
| 282 | + return $strength; | |
| 283 | + } | |
| 284 | + | |
| 285 | + /** | |
| 286 | + * Private helper methods | |
| 287 | + */ | |
| 288 | + private static function get_client_ip() | |
| 289 | + { | |
| 290 | + $ip_keys = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP', 'REMOTE_ADDR']; | |
| 291 | + | |
| 292 | + foreach ($ip_keys as $key) { | |
| 293 | + if (!empty($_SERVER[$key])) { | |
| 294 | + $ip = trim($_SERVER[$key]); | |
| 295 | + // Handle comma-separated IPs (from load balancers) | |
| 296 | + if (strpos($ip, ',') !== false) { | |
| 297 | + $ip = trim(explode(',', $ip)[0]); | |
| 298 | + } | |
| 299 | + if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { | |
| 300 | + return $ip; | |
| 301 | + } | |
| 302 | + } | |
| 303 | + } | |
| 304 | + | |
| 305 | + return $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'; | |
| 306 | + } | |
| 307 | + | |
| 308 | + private static function get_max_attempts($action) | |
| 309 | + { | |
| 310 | + switch ($action) { | |
| 311 | + case 'login': | |
| 312 | + return self::MAX_LOGIN_ATTEMPTS; | |
| 313 | + case 'register': | |
| 314 | + return self::MAX_REGISTER_ATTEMPTS; | |
| 315 | + case 'lostpassword': | |
| 316 | + return self::MAX_LOST_PASSWORD_ATTEMPTS; | |
| 317 | + default: | |
| 318 | + return 3; | |
| 319 | + } | |
| 320 | + } | |
| 321 | + | |
| 322 | + private static function contains_malicious_content($content) | |
| 323 | + { | |
| 324 | + $malicious_patterns = [ | |
| 325 | + '/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/mi', | |
| 326 | + '/javascript:/i', | |
| 327 | + '/data:text\/html/i', | |
| 328 | + '/\bon\w+\s*=/i', // Event handlers like onclick | |
| 329 | + '/eval\s*\(/i', | |
| 330 | + '/exec\s*\(/i' | |
| 331 | + ]; | |
| 332 | + | |
| 333 | + foreach ($malicious_patterns as $pattern) { | |
| 334 | + if (preg_match($pattern, $content)) { | |
| 335 | + return true; | |
| 336 | + } | |
| 337 | + } | |
| 338 | + | |
| 339 | + return false; | |
| 340 | + } | |
| 341 | + | |
| 342 | + private static function is_safe_image_url($url) | |
| 343 | + { | |
| 344 | + // Only allow images from trusted domains | |
| 345 | + $trusted_domains = [ | |
| 346 | + 'lh3.googleusercontent.com', // Google profile pictures | |
| 347 | + 'platform-lookaside.fbsbx.com', // Facebook profile pictures | |
| 348 | + 'graph.facebook.com', // Facebook graph API | |
| 349 | + 'scontent.xx.fbcdn.net' // Facebook CDN | |
| 350 | + ]; | |
| 351 | + | |
| 352 | + $parsed_url = parse_url($url); | |
| 353 | + $domain = $parsed_url['host'] ?? ''; | |
| 354 | + | |
| 355 | + return in_array($domain, $trusted_domains); | |
| 356 | + } | |
| 357 | + | |
| 358 | + private static function is_safe_email_domain($email) | |
| 359 | + { | |
| 360 | + // Check against known suspicious domains | |
| 361 | + $suspicious_domains = [ | |
| 362 | + 'guerrillamail.com', | |
| 363 | + '10minutemail.com', | |
| 364 | + 'mailinator.com', | |
| 365 | + 'tempmail.org' | |
| 366 | + ]; | |
| 367 | + | |
| 368 | + $domain = substr(strrchr($email, "@"), 1); | |
| 369 | + return !in_array(strtolower($domain), $suspicious_domains); | |
| 370 | + } | |
| 371 | + | |
| 372 | + private static function is_common_password($password) | |
| 373 | + { | |
| 374 | + $common_passwords = [ | |
| 375 | + 'password', '123456', '123456789', 'qwerty', 'abc123', | |
| 376 | + 'password123', 'admin', 'letmein', 'welcome', 'monkey', | |
| 377 | + 'dragon', 'master', 'sunshine', 'princess', 'football' | |
| 378 | + ]; | |
| 379 | + | |
| 380 | + return in_array(strtolower($password), $common_passwords); | |
| 381 | + } | |
| 382 | +} | |