Changeset 3452601
- Timestamp:
- 02/03/2026 06:44:53 AM (5 weeks ago)
- Location:
- basecloud-shield
- Files:
-
- 2 added
- 6 edited
- 1 copied
-
tags/1.2.7 (copied) (copied from basecloud-shield/trunk)
-
tags/1.2.7/CHANGELOG.md (added)
-
tags/1.2.7/basecloud-shield.php (modified) (15 diffs)
-
tags/1.2.7/package.json (modified) (1 diff)
-
tags/1.2.7/readme.txt (modified) (2 diffs)
-
trunk/CHANGELOG.md (added)
-
trunk/basecloud-shield.php (modified) (15 diffs)
-
trunk/package.json (modified) (1 diff)
-
trunk/readme.txt (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
basecloud-shield/tags/1.2.7/basecloud-shield.php
r3442655 r3452601 3 3 * Plugin Name: BaseCloud Shield 4 4 * Description: Enterprise-grade 2FA security. Supports Central Manager Notifications, WP Email, SendGrid, WhatsApp, SMS, and Webhooks. 5 * Version: 1.2. 65 * Version: 1.2.7 6 6 * Author: BaseCloud Team 7 7 * Author URI: https://www.basecloudglobal.com/ … … 15 15 if (!defined('ABSPATH')) { exit; } 16 16 17 define('BCSHIELD_VERSION', '1.2.6'); 17 define('BCSHIELD_VERSION', '1.2.7'); 18 define('BCSHIELD_MAX_ATTEMPTS', 5); 19 define('BCSHIELD_LOCKOUT_DURATION', 900); 20 define('BCSHIELD_OTP_RATE_LIMIT', 3); 21 define('BCSHIELD_OTP_RATE_WINDOW', 600); 18 22 19 23 class BaseCloudShield { … … 107 111 if (is_wp_error($user)) return $user; 108 112 109 // Check verification cookie (Browser Trust) 110 if (isset($_COOKIE['bcshield_2fa_verified']) && $_COOKIE['bcshield_2fa_verified'] === md5($user->ID . 'bcshield_trust')) return $user; 111 112 // Generate OTP 113 $otp = rand(100000, 999999); 113 $client_ip = $this->get_client_ip(); 114 $user_agent = $this->get_user_agent(); 115 116 // Security: Check if IP is locked out due to too many failed attempts 117 if ($this->is_ip_locked_out($client_ip)) { 118 $this->log_security_event('ip_lockout', $user->ID, $client_ip, 'IP locked out due to multiple failed attempts'); 119 return new WP_Error('ip_locked', 'Too many failed attempts. Please try again later.'); 120 } 121 122 // Security: Rate limit OTP generation per user 123 if ($this->is_otp_rate_limited($user->ID, $client_ip)) { 124 $this->log_security_event('rate_limited', $user->ID, $client_ip, 'OTP generation rate limited'); 125 return new WP_Error('rate_limited', 'Too many OTP requests. Please wait before trying again.'); 126 } 127 128 // Check verification cookie (Browser Trust) with enhanced security 129 if (isset($_COOKIE['bcshield_2fa_verified'])) { 130 $expected_hash = $this->generate_trust_hash($user->ID, $client_ip, $user_agent); 131 if (hash_equals($expected_hash, $_COOKIE['bcshield_2fa_verified'])) { 132 $this->log_security_event('trusted_login', $user->ID, $client_ip, 'Logged in using trusted device'); 133 return $user; 134 } else { 135 // Invalid trust cookie - potential session hijacking attempt 136 $this->log_security_event('invalid_trust_cookie', $user->ID, $client_ip, 'Invalid trust cookie detected'); 137 $this->send_security_alert($user, 'Suspicious login attempt detected', $client_ip); 138 setcookie('bcshield_2fa_verified', '', time() - 3600, '/', '', true, true); 139 } 140 } 141 142 // Prevent duplicate OTP generation (lock mechanism) 143 $lock_key = 'bcshield_otp_lock_' . $user->ID; 144 if (get_transient($lock_key)) { 145 // OTP already generated recently, skip duplicate 146 return $user; 147 } 148 149 // Set lock for 60 seconds to prevent duplicate sends 150 set_transient($lock_key, true, 60); 151 152 // Generate cryptographically secure OTP 153 $otp = $this->generate_secure_otp(); 114 154 $validity_min = $opts['otp_validity'] ?? 10; 115 set_transient('bcshield_otp_' . $user->ID, $otp, $validity_min * 60); 155 156 // Store OTP with metadata for security validation 157 $otp_data = array( 158 'code' => $otp, 159 'ip' => $client_ip, 160 'user_agent' => $user_agent, 161 'timestamp' => time(), 162 'attempts' => 0 163 ); 164 set_transient('bcshield_otp_' . $user->ID, $otp_data, $validity_min * 60); 165 166 // Track OTP generation for rate limiting 167 $this->track_otp_generation($user->ID, $client_ip); 168 169 // Log security event 170 $this->log_security_event('otp_generated', $user->ID, $client_ip, 'OTP generated for login'); 116 171 117 172 // ROUTING LOGIC: Determine Recipients … … 121 176 $delivery_methods = isset($opts['delivery_methods']) ? $opts['delivery_methods'] : array('email'); 122 177 $from_email = $opts['from_email'] ?? get_bloginfo('admin_email'); 178 179 // Track sent emails to avoid duplicates 180 $sent_to_emails = array(); 181 $sent_to_phones = array(); 123 182 124 183 foreach ($delivery_methods as $method) { … … 126 185 case 'email': 127 186 foreach ($recipients as $recipient) { 128 $this->send_via_email($user, $otp, $recipient['email'], $from_email); 187 if (!in_array($recipient['email'], $sent_to_emails)) { 188 $this->send_via_email($user, $otp, $recipient['email'], $from_email); 189 $sent_to_emails[] = $recipient['email']; 190 } 129 191 } 130 192 break; … … 134 196 $sendgrid_from = $opts['sendgrid_from_email'] ?? $from_email; 135 197 foreach ($recipients as $recipient) { 136 $this->send_via_sendgrid($user, $otp, $recipient['email'], $opts['sendgrid_key'], $sendgrid_from); 198 if (!in_array($recipient['email'], $sent_to_emails)) { 199 $this->send_via_sendgrid($user, $otp, $recipient['email'], $opts['sendgrid_key'], $sendgrid_from); 200 $sent_to_emails[] = $recipient['email']; 201 } 137 202 } 138 203 } … … 148 213 if (!empty($opts['whatsapp_account_sid']) && !empty($opts['whatsapp_auth_token'])) { 149 214 foreach ($recipients as $recipient) { 150 if (!empty($recipient['phone']) ) {215 if (!empty($recipient['phone']) && !in_array($recipient['phone'], $sent_to_phones)) { 151 216 $this->send_via_whatsapp($user, $otp, $recipient['phone'], $opts); 217 $sent_to_phones[] = $recipient['phone']; 152 218 } 153 219 } … … 158 224 if (!empty($opts['sms_account_sid']) && !empty($opts['sms_auth_token'])) { 159 225 foreach ($recipients as $recipient) { 160 if (!empty($recipient['phone']) ) {226 if (!empty($recipient['phone']) && !in_array($recipient['phone'], $sent_to_phones)) { 161 227 $this->send_via_sms($user, $otp, $recipient['phone'], $opts); 228 $sent_to_phones[] = $recipient['phone']; 162 229 } 163 230 } … … 167 234 } 168 235 169 // Redirect 170 setcookie('bcshield_pending_user', $user->ID, time() + ($validity_min * 60), '/'); 236 // Redirect with enhanced security 237 $session_token = $this->generate_session_token($user->ID, $client_ip, $user_agent); 238 setcookie('bcshield_pending_user', $user->ID, time() + ($validity_min * 60), '/', '', true, true); 239 setcookie('bcshield_session', $session_token, time() + ($validity_min * 60), '/', '', true, true); 240 171 241 $base_url = site_url(); 172 242 $redirect = add_query_arg('bcshield_action', 'verify_otp', $base_url); … … 177 247 private function get_otp_recipients($user, $opts) { 178 248 $recipients = array(); 249 $seen_emails = array(); 250 $seen_phones = array(); 179 251 $mode = $opts['recipient_mode'] ?? 'user'; 180 252 … … 187 259 ); 188 260 } elseif ($mode === 'selected' && !empty($opts['selected_users'])) { 189 // Send to selected users 261 // Send to selected users (with deduplication) 190 262 foreach ($opts['selected_users'] as $user_id) { 191 263 $selected_user = get_userdata($user_id); 192 if ($selected_user ) {264 if ($selected_user && !in_array($selected_user->user_email, $seen_emails)) { 193 265 $recipients[] = array( 194 266 'email' => $selected_user->user_email, 195 267 'phone' => get_user_meta($user_id, 'billing_phone', true) 196 268 ); 269 $seen_emails[] = $selected_user->user_email; 197 270 } 198 271 } … … 208 281 } 209 282 283 // --- SECURITY METHODS --- 284 285 private function generate_secure_otp() { 286 // Use cryptographically secure random number generation 287 try { 288 $bytes = random_bytes(4); 289 $otp = abs(unpack('l', $bytes)[1]) % 900000 + 100000; 290 } catch (Exception $e) { 291 // Fallback to mt_rand if random_bytes fails 292 $otp = mt_rand(100000, 999999); 293 } 294 return $otp; 295 } 296 297 private function generate_trust_hash($user_id, $ip, $user_agent) { 298 $secret = wp_salt('auth') . BCSHIELD_VERSION; 299 return hash_hmac('sha256', $user_id . $ip . $user_agent, $secret); 300 } 301 302 private function generate_session_token($user_id, $ip, $user_agent) { 303 $secret = wp_salt('nonce') . time(); 304 return hash_hmac('sha256', $user_id . $ip . $user_agent, $secret); 305 } 306 307 private function get_client_ip() { 308 $ip = ''; 309 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { 310 $ip = sanitize_text_field($_SERVER['HTTP_X_FORWARDED_FOR']); 311 $ip = explode(',', $ip)[0]; // Take first IP if multiple 312 } elseif (!empty($_SERVER['HTTP_CLIENT_IP'])) { 313 $ip = sanitize_text_field($_SERVER['HTTP_CLIENT_IP']); 314 } elseif (!empty($_SERVER['REMOTE_ADDR'])) { 315 $ip = sanitize_text_field($_SERVER['REMOTE_ADDR']); 316 } 317 return filter_var($ip, FILTER_VALIDATE_IP) ? $ip : '0.0.0.0'; 318 } 319 320 private function get_user_agent() { 321 return isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field($_SERVER['HTTP_USER_AGENT']) : ''; 322 } 323 324 private function is_ip_locked_out($ip) { 325 $lockout = get_transient('bcshield_lockout_' . md5($ip)); 326 return $lockout !== false; 327 } 328 329 private function lock_out_ip($ip) { 330 set_transient('bcshield_lockout_' . md5($ip), true, BCSHIELD_LOCKOUT_DURATION); 331 } 332 333 private function is_otp_rate_limited($user_id, $ip) { 334 $key = 'bcshield_otp_rate_' . md5($user_id . $ip); 335 $attempts = get_transient($key); 336 return $attempts !== false && $attempts >= BCSHIELD_OTP_RATE_LIMIT; 337 } 338 339 private function track_otp_generation($user_id, $ip) { 340 $key = 'bcshield_otp_rate_' . md5($user_id . $ip); 341 $attempts = get_transient($key); 342 $attempts = $attempts ? $attempts + 1 : 1; 343 set_transient($key, $attempts, BCSHIELD_OTP_RATE_WINDOW); 344 } 345 346 private function log_security_event($event_type, $user_id, $ip, $details) { 347 $log_entry = array( 348 'timestamp' => current_time('mysql'), 349 'event' => $event_type, 350 'user_id' => $user_id, 351 'ip' => $ip, 352 'details' => $details, 353 'user_agent' => $this->get_user_agent() 354 ); 355 356 $logs = get_option('bcshield_security_logs', array()); 357 array_unshift($logs, $log_entry); 358 359 // Keep only last 100 events 360 $logs = array_slice($logs, 0, 100); 361 update_option('bcshield_security_logs', $logs, false); 362 } 363 364 private function send_security_alert($user, $alert_type, $ip) { 365 $opts = get_option($this->option_name); 366 $delivery_methods = isset($opts['delivery_methods']) ? $opts['delivery_methods'] : array('email'); 367 368 // Only send alerts via email or webhook to avoid SMS spam 369 if (in_array('email', $delivery_methods)) { 370 $subject = '🚨 Security Alert: ' . $alert_type; 371 $message = $this->get_security_alert_template($user, $alert_type, $ip); 372 $from_email = $opts['from_email'] ?? get_bloginfo('admin_email'); 373 $headers = array('Content-Type: text/html; charset=UTF-8', 'From: ' . get_bloginfo('name') . ' Security <' . $from_email . '>'); 374 wp_mail($user->user_email, $subject, $message, $headers); 375 } 376 377 if (in_array('webhook', $delivery_methods) && !empty($opts['webhook_url'])) { 378 $body = array( 379 'alert_type' => $alert_type, 380 'site_name' => get_bloginfo('name'), 381 'username' => $user->user_login, 382 'email' => $user->user_email, 383 'ip_address' => $ip, 384 'timestamp' => current_time('mysql'), 385 'severity' => 'high' 386 ); 387 wp_remote_post($opts['webhook_url'], array('body' => $body, 'blocking' => false)); 388 } 389 } 390 391 private function get_security_alert_template($user, $alert_type, $ip) { 392 return " 393 <div style='background:#f5f5f5; padding:30px; font-family:sans-serif;'> 394 <div style='background:#fff; padding:30px; border-radius:10px; max-width:500px; margin:0 auto; border-top: 5px solid #e74c3c;'> 395 <h2 style='color:#e74c3c; margin-top:0;'>🚨 Security Alert</h2> 396 <p style='color:#666;'> 397 <strong>Site:</strong> " . get_bloginfo('name') . "<br> 398 <strong>User:</strong> " . esc_html($user->user_login) . "<br> 399 <strong>Alert:</strong> " . esc_html($alert_type) . "<br> 400 <strong>IP Address:</strong> " . esc_html($ip) . "<br> 401 <strong>Time:</strong> " . current_time('mysql') . " 402 </p> 403 <div style='background:#fee; border-left:4px solid #e74c3c; padding:15px; margin:20px 0;'> 404 <strong>What should you do?</strong><br> 405 If this wasn't you, please change your password immediately and contact your site administrator. 406 </div> 407 <p style='color:#999; font-size:12px;'>Secured by BaseCloud Shield</p> 408 </div> 409 </div>"; 410 } 411 210 412 // --- 3. DELIVERY METHODS --- 211 413 … … 326 528 <?php endif; ?> 327 529 <form method="post"> 328 <input type="text" name="otp_input" autocomplete="off" autofocus required placeholder="000000" maxlength="6" /> 530 <?php wp_nonce_field('bcshield_verify_otp', 'bcshield_nonce'); ?> 531 <input type="text" name="otp_input" autocomplete="off" autofocus required placeholder="000000" maxlength="6" pattern="[0-9]{6}" inputmode="numeric" /> 329 532 <button type="submit" name="bcshield_otp_submit">Verify & Login</button> 330 533 </form> … … 339 542 340 543 private function validate_otp() { 341 if (!isset($_COOKIE['bcshield_pending_user']) ) {544 if (!isset($_COOKIE['bcshield_pending_user']) || !isset($_COOKIE['bcshield_session'])) { 342 545 wp_redirect(home_url()); 343 546 exit; … … 345 548 346 549 $user_id = intval($_COOKIE['bcshield_pending_user']); 347 $correct_otp = get_transient('bcshield_otp_' . $user_id); 550 $client_ip = $this->get_client_ip(); 551 $user_agent = $this->get_user_agent(); 552 553 // Verify CSRF nonce 554 if (!isset($_POST['bcshield_nonce']) || !wp_verify_nonce($_POST['bcshield_nonce'], 'bcshield_verify_otp')) { 555 $this->log_security_event('csrf_attempt', $user_id, $client_ip, 'CSRF token validation failed'); 556 wp_redirect(home_url()); 557 exit; 558 } 559 560 // Verify session token to prevent session fixation 561 $expected_session = $this->generate_session_token($user_id, $client_ip, $user_agent); 562 if (!hash_equals($expected_session, $_COOKIE['bcshield_session'])) { 563 $this->log_security_event('session_mismatch', $user_id, $client_ip, 'Session token mismatch detected'); 564 $this->send_security_alert(get_userdata($user_id), 'Suspicious session detected', $client_ip); 565 wp_redirect(home_url()); 566 exit; 567 } 568 569 $otp_data = get_transient('bcshield_otp_' . $user_id); 348 570 $user_input = sanitize_text_field($_POST['otp_input']); 349 350 if ($correct_otp && $user_input == $correct_otp) { 351 setcookie('bcshield_2fa_verified', md5($user_id . 'bcshield_trust'), time() + (12 * 3600), '/'); 571 572 if (!$otp_data) { 573 $this->otp_error = 'Code expired. Please login again.'; 574 $this->log_security_event('otp_expired', $user_id, $client_ip, 'Attempted to use expired OTP'); 575 return; 576 } 577 578 // Check if too many attempts 579 if ($otp_data['attempts'] >= BCSHIELD_MAX_ATTEMPTS) { 352 580 delete_transient('bcshield_otp_' . $user_id); 353 setcookie('bcshield_pending_user', '', time() - 3600, '/'); 581 $this->lock_out_ip($client_ip); 582 $this->log_security_event('max_attempts', $user_id, $client_ip, 'Maximum OTP attempts exceeded'); 583 $this->send_security_alert(get_userdata($user_id), 'Multiple failed OTP attempts', $client_ip); 584 $this->otp_error = 'Too many failed attempts. Account temporarily locked.'; 585 return; 586 } 587 588 // Verify IP and User Agent match (prevent OTP interception) 589 if ($otp_data['ip'] !== $client_ip) { 590 $this->log_security_event('ip_mismatch', $user_id, $client_ip, 'OTP verification from different IP: ' . $otp_data['ip']); 591 $this->send_security_alert(get_userdata($user_id), 'OTP accessed from different IP', $client_ip); 592 $this->otp_error = 'Security validation failed. Please login again.'; 593 delete_transient('bcshield_otp_' . $user_id); 594 return; 595 } 596 597 if (hash_equals((string)$otp_data['code'], $user_input)) { 598 // Success - Create trusted device token 599 $trust_hash = $this->generate_trust_hash($user_id, $client_ip, $user_agent); 600 setcookie('bcshield_2fa_verified', $trust_hash, time() + (12 * 3600), '/', '', true, true); 601 602 delete_transient('bcshield_otp_' . $user_id); 603 setcookie('bcshield_pending_user', '', time() - 3600, '/', '', true, true); 604 setcookie('bcshield_session', '', time() - 3600, '/', '', true, true); 605 606 $this->log_security_event('otp_success', $user_id, $client_ip, 'OTP verified successfully'); 607 354 608 wp_set_auth_cookie($user_id); 355 609 wp_redirect(admin_url()); 356 610 exit; 357 611 } else { 358 $this->otp_error = 'Incorrect Code. Please try again.'; 612 // Failed attempt - increment counter 613 $otp_data['attempts']++; 614 $validity_remaining = get_option('_transient_timeout_bcshield_otp_' . $user_id) - time(); 615 set_transient('bcshield_otp_' . $user_id, $otp_data, $validity_remaining); 616 617 $remaining_attempts = BCSHIELD_MAX_ATTEMPTS - $otp_data['attempts']; 618 $this->log_security_event('otp_failed', $user_id, $client_ip, 'Failed OTP attempt ' . $otp_data['attempts']); 619 620 $this->otp_error = 'Incorrect code. ' . $remaining_attempts . ' attempt(s) remaining.'; 359 621 } 360 622 } -
basecloud-shield/tags/1.2.7/package.json
r3442419 r3452601 1 1 { 2 2 "name": "basecloud-shield", 3 "version": "1.2. 4",3 "version": "1.2.7", 4 4 "description": "WordPress 2FA Security Plugin - Build and deployment scripts", 5 5 "scripts": { -
basecloud-shield/tags/1.2.7/readme.txt
r3442655 r3452601 4 4 Requires at least: 5.0 5 5 Tested up to: 6.9 6 Stable tag: 1.2. 66 Stable tag: 1.2.7 7 7 Requires PHP: 7.4 8 8 License: GPLv2 or later … … 118 118 119 119 == Changelog == 120 121 = 1.2.7 = 122 **Critical Security & Bug Fix Release** 123 124 **CRITICAL FIX - Duplicate OTP Prevention:** 125 • Fixed issue causing multiple duplicate OTP emails to be sent 126 • Implemented email deduplication across all delivery methods 127 • Added phone number deduplication for WhatsApp/SMS 128 • Enhanced recipient list processing to prevent duplicate entries 129 • Added 60-second OTP generation lock to prevent rapid duplicates 130 131 **Enterprise-Grade Security Enhancements:** 132 • Brute Force Protection: Maximum 5 OTP attempts before 15-minute IP lockout 133 • Rate Limiting: 3 OTP requests per 10-minute window per user/IP 134 • Cryptographically Secure OTP: Replaced rand() with random_bytes() 135 • Session Binding: IP address validation, User-Agent fingerprinting 136 • HMAC-SHA256 session tokens to prevent session fixation attacks 137 • CSRF Protection: WordPress nonce validation on all OTP submissions 138 • Enhanced Cookie Security: httponly and secure flags on all cookies 139 • Security Event Logging: Comprehensive audit trail (last 100 events) 140 • Real-Time Security Alerts: Email/webhook alerts for suspicious activity 141 • Timing Attack Protection: Constant-time comparisons using hash_equals() 142 143 **Attack Prevention:** 144 • OTP Interception Prevention (IP binding) 145 • Session Hijacking Detection (multi-factor validation) 146 • CSRF Attack Protection (nonce tokens) 147 • Replay Attack Prevention (one-time codes with metadata) 148 • Rate Limit Abuse Prevention (throttling) 149 • Brute Force Attack Blocking (auto-lockout) 150 151 **Security Monitoring:** 152 • 12 new security event types tracked and logged 153 • IP mismatch detection and alerting 154 • Session token mismatch detection 155 • Failed attempt tracking with remaining attempt counter 156 • Expired OTP usage attempt logging 157 • Invalid trust cookie detection 158 159 **Technical Improvements:** 160 • Enhanced IP detection (proxy, CloudFlare, load balancer support) 161 • OTP metadata tracking (IP, User-Agent, timestamp, attempts) 162 • Improved error messages with security context 163 • Pattern validation for numeric OTP input 164 • Better cookie management with expiration handling 120 165 121 166 = 1.2.6 = -
basecloud-shield/trunk/basecloud-shield.php
r3442655 r3452601 3 3 * Plugin Name: BaseCloud Shield 4 4 * Description: Enterprise-grade 2FA security. Supports Central Manager Notifications, WP Email, SendGrid, WhatsApp, SMS, and Webhooks. 5 * Version: 1.2. 65 * Version: 1.2.7 6 6 * Author: BaseCloud Team 7 7 * Author URI: https://www.basecloudglobal.com/ … … 15 15 if (!defined('ABSPATH')) { exit; } 16 16 17 define('BCSHIELD_VERSION', '1.2.6'); 17 define('BCSHIELD_VERSION', '1.2.7'); 18 define('BCSHIELD_MAX_ATTEMPTS', 5); 19 define('BCSHIELD_LOCKOUT_DURATION', 900); 20 define('BCSHIELD_OTP_RATE_LIMIT', 3); 21 define('BCSHIELD_OTP_RATE_WINDOW', 600); 18 22 19 23 class BaseCloudShield { … … 107 111 if (is_wp_error($user)) return $user; 108 112 109 // Check verification cookie (Browser Trust) 110 if (isset($_COOKIE['bcshield_2fa_verified']) && $_COOKIE['bcshield_2fa_verified'] === md5($user->ID . 'bcshield_trust')) return $user; 111 112 // Generate OTP 113 $otp = rand(100000, 999999); 113 $client_ip = $this->get_client_ip(); 114 $user_agent = $this->get_user_agent(); 115 116 // Security: Check if IP is locked out due to too many failed attempts 117 if ($this->is_ip_locked_out($client_ip)) { 118 $this->log_security_event('ip_lockout', $user->ID, $client_ip, 'IP locked out due to multiple failed attempts'); 119 return new WP_Error('ip_locked', 'Too many failed attempts. Please try again later.'); 120 } 121 122 // Security: Rate limit OTP generation per user 123 if ($this->is_otp_rate_limited($user->ID, $client_ip)) { 124 $this->log_security_event('rate_limited', $user->ID, $client_ip, 'OTP generation rate limited'); 125 return new WP_Error('rate_limited', 'Too many OTP requests. Please wait before trying again.'); 126 } 127 128 // Check verification cookie (Browser Trust) with enhanced security 129 if (isset($_COOKIE['bcshield_2fa_verified'])) { 130 $expected_hash = $this->generate_trust_hash($user->ID, $client_ip, $user_agent); 131 if (hash_equals($expected_hash, $_COOKIE['bcshield_2fa_verified'])) { 132 $this->log_security_event('trusted_login', $user->ID, $client_ip, 'Logged in using trusted device'); 133 return $user; 134 } else { 135 // Invalid trust cookie - potential session hijacking attempt 136 $this->log_security_event('invalid_trust_cookie', $user->ID, $client_ip, 'Invalid trust cookie detected'); 137 $this->send_security_alert($user, 'Suspicious login attempt detected', $client_ip); 138 setcookie('bcshield_2fa_verified', '', time() - 3600, '/', '', true, true); 139 } 140 } 141 142 // Prevent duplicate OTP generation (lock mechanism) 143 $lock_key = 'bcshield_otp_lock_' . $user->ID; 144 if (get_transient($lock_key)) { 145 // OTP already generated recently, skip duplicate 146 return $user; 147 } 148 149 // Set lock for 60 seconds to prevent duplicate sends 150 set_transient($lock_key, true, 60); 151 152 // Generate cryptographically secure OTP 153 $otp = $this->generate_secure_otp(); 114 154 $validity_min = $opts['otp_validity'] ?? 10; 115 set_transient('bcshield_otp_' . $user->ID, $otp, $validity_min * 60); 155 156 // Store OTP with metadata for security validation 157 $otp_data = array( 158 'code' => $otp, 159 'ip' => $client_ip, 160 'user_agent' => $user_agent, 161 'timestamp' => time(), 162 'attempts' => 0 163 ); 164 set_transient('bcshield_otp_' . $user->ID, $otp_data, $validity_min * 60); 165 166 // Track OTP generation for rate limiting 167 $this->track_otp_generation($user->ID, $client_ip); 168 169 // Log security event 170 $this->log_security_event('otp_generated', $user->ID, $client_ip, 'OTP generated for login'); 116 171 117 172 // ROUTING LOGIC: Determine Recipients … … 121 176 $delivery_methods = isset($opts['delivery_methods']) ? $opts['delivery_methods'] : array('email'); 122 177 $from_email = $opts['from_email'] ?? get_bloginfo('admin_email'); 178 179 // Track sent emails to avoid duplicates 180 $sent_to_emails = array(); 181 $sent_to_phones = array(); 123 182 124 183 foreach ($delivery_methods as $method) { … … 126 185 case 'email': 127 186 foreach ($recipients as $recipient) { 128 $this->send_via_email($user, $otp, $recipient['email'], $from_email); 187 if (!in_array($recipient['email'], $sent_to_emails)) { 188 $this->send_via_email($user, $otp, $recipient['email'], $from_email); 189 $sent_to_emails[] = $recipient['email']; 190 } 129 191 } 130 192 break; … … 134 196 $sendgrid_from = $opts['sendgrid_from_email'] ?? $from_email; 135 197 foreach ($recipients as $recipient) { 136 $this->send_via_sendgrid($user, $otp, $recipient['email'], $opts['sendgrid_key'], $sendgrid_from); 198 if (!in_array($recipient['email'], $sent_to_emails)) { 199 $this->send_via_sendgrid($user, $otp, $recipient['email'], $opts['sendgrid_key'], $sendgrid_from); 200 $sent_to_emails[] = $recipient['email']; 201 } 137 202 } 138 203 } … … 148 213 if (!empty($opts['whatsapp_account_sid']) && !empty($opts['whatsapp_auth_token'])) { 149 214 foreach ($recipients as $recipient) { 150 if (!empty($recipient['phone']) ) {215 if (!empty($recipient['phone']) && !in_array($recipient['phone'], $sent_to_phones)) { 151 216 $this->send_via_whatsapp($user, $otp, $recipient['phone'], $opts); 217 $sent_to_phones[] = $recipient['phone']; 152 218 } 153 219 } … … 158 224 if (!empty($opts['sms_account_sid']) && !empty($opts['sms_auth_token'])) { 159 225 foreach ($recipients as $recipient) { 160 if (!empty($recipient['phone']) ) {226 if (!empty($recipient['phone']) && !in_array($recipient['phone'], $sent_to_phones)) { 161 227 $this->send_via_sms($user, $otp, $recipient['phone'], $opts); 228 $sent_to_phones[] = $recipient['phone']; 162 229 } 163 230 } … … 167 234 } 168 235 169 // Redirect 170 setcookie('bcshield_pending_user', $user->ID, time() + ($validity_min * 60), '/'); 236 // Redirect with enhanced security 237 $session_token = $this->generate_session_token($user->ID, $client_ip, $user_agent); 238 setcookie('bcshield_pending_user', $user->ID, time() + ($validity_min * 60), '/', '', true, true); 239 setcookie('bcshield_session', $session_token, time() + ($validity_min * 60), '/', '', true, true); 240 171 241 $base_url = site_url(); 172 242 $redirect = add_query_arg('bcshield_action', 'verify_otp', $base_url); … … 177 247 private function get_otp_recipients($user, $opts) { 178 248 $recipients = array(); 249 $seen_emails = array(); 250 $seen_phones = array(); 179 251 $mode = $opts['recipient_mode'] ?? 'user'; 180 252 … … 187 259 ); 188 260 } elseif ($mode === 'selected' && !empty($opts['selected_users'])) { 189 // Send to selected users 261 // Send to selected users (with deduplication) 190 262 foreach ($opts['selected_users'] as $user_id) { 191 263 $selected_user = get_userdata($user_id); 192 if ($selected_user ) {264 if ($selected_user && !in_array($selected_user->user_email, $seen_emails)) { 193 265 $recipients[] = array( 194 266 'email' => $selected_user->user_email, 195 267 'phone' => get_user_meta($user_id, 'billing_phone', true) 196 268 ); 269 $seen_emails[] = $selected_user->user_email; 197 270 } 198 271 } … … 208 281 } 209 282 283 // --- SECURITY METHODS --- 284 285 private function generate_secure_otp() { 286 // Use cryptographically secure random number generation 287 try { 288 $bytes = random_bytes(4); 289 $otp = abs(unpack('l', $bytes)[1]) % 900000 + 100000; 290 } catch (Exception $e) { 291 // Fallback to mt_rand if random_bytes fails 292 $otp = mt_rand(100000, 999999); 293 } 294 return $otp; 295 } 296 297 private function generate_trust_hash($user_id, $ip, $user_agent) { 298 $secret = wp_salt('auth') . BCSHIELD_VERSION; 299 return hash_hmac('sha256', $user_id . $ip . $user_agent, $secret); 300 } 301 302 private function generate_session_token($user_id, $ip, $user_agent) { 303 $secret = wp_salt('nonce') . time(); 304 return hash_hmac('sha256', $user_id . $ip . $user_agent, $secret); 305 } 306 307 private function get_client_ip() { 308 $ip = ''; 309 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { 310 $ip = sanitize_text_field($_SERVER['HTTP_X_FORWARDED_FOR']); 311 $ip = explode(',', $ip)[0]; // Take first IP if multiple 312 } elseif (!empty($_SERVER['HTTP_CLIENT_IP'])) { 313 $ip = sanitize_text_field($_SERVER['HTTP_CLIENT_IP']); 314 } elseif (!empty($_SERVER['REMOTE_ADDR'])) { 315 $ip = sanitize_text_field($_SERVER['REMOTE_ADDR']); 316 } 317 return filter_var($ip, FILTER_VALIDATE_IP) ? $ip : '0.0.0.0'; 318 } 319 320 private function get_user_agent() { 321 return isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field($_SERVER['HTTP_USER_AGENT']) : ''; 322 } 323 324 private function is_ip_locked_out($ip) { 325 $lockout = get_transient('bcshield_lockout_' . md5($ip)); 326 return $lockout !== false; 327 } 328 329 private function lock_out_ip($ip) { 330 set_transient('bcshield_lockout_' . md5($ip), true, BCSHIELD_LOCKOUT_DURATION); 331 } 332 333 private function is_otp_rate_limited($user_id, $ip) { 334 $key = 'bcshield_otp_rate_' . md5($user_id . $ip); 335 $attempts = get_transient($key); 336 return $attempts !== false && $attempts >= BCSHIELD_OTP_RATE_LIMIT; 337 } 338 339 private function track_otp_generation($user_id, $ip) { 340 $key = 'bcshield_otp_rate_' . md5($user_id . $ip); 341 $attempts = get_transient($key); 342 $attempts = $attempts ? $attempts + 1 : 1; 343 set_transient($key, $attempts, BCSHIELD_OTP_RATE_WINDOW); 344 } 345 346 private function log_security_event($event_type, $user_id, $ip, $details) { 347 $log_entry = array( 348 'timestamp' => current_time('mysql'), 349 'event' => $event_type, 350 'user_id' => $user_id, 351 'ip' => $ip, 352 'details' => $details, 353 'user_agent' => $this->get_user_agent() 354 ); 355 356 $logs = get_option('bcshield_security_logs', array()); 357 array_unshift($logs, $log_entry); 358 359 // Keep only last 100 events 360 $logs = array_slice($logs, 0, 100); 361 update_option('bcshield_security_logs', $logs, false); 362 } 363 364 private function send_security_alert($user, $alert_type, $ip) { 365 $opts = get_option($this->option_name); 366 $delivery_methods = isset($opts['delivery_methods']) ? $opts['delivery_methods'] : array('email'); 367 368 // Only send alerts via email or webhook to avoid SMS spam 369 if (in_array('email', $delivery_methods)) { 370 $subject = '🚨 Security Alert: ' . $alert_type; 371 $message = $this->get_security_alert_template($user, $alert_type, $ip); 372 $from_email = $opts['from_email'] ?? get_bloginfo('admin_email'); 373 $headers = array('Content-Type: text/html; charset=UTF-8', 'From: ' . get_bloginfo('name') . ' Security <' . $from_email . '>'); 374 wp_mail($user->user_email, $subject, $message, $headers); 375 } 376 377 if (in_array('webhook', $delivery_methods) && !empty($opts['webhook_url'])) { 378 $body = array( 379 'alert_type' => $alert_type, 380 'site_name' => get_bloginfo('name'), 381 'username' => $user->user_login, 382 'email' => $user->user_email, 383 'ip_address' => $ip, 384 'timestamp' => current_time('mysql'), 385 'severity' => 'high' 386 ); 387 wp_remote_post($opts['webhook_url'], array('body' => $body, 'blocking' => false)); 388 } 389 } 390 391 private function get_security_alert_template($user, $alert_type, $ip) { 392 return " 393 <div style='background:#f5f5f5; padding:30px; font-family:sans-serif;'> 394 <div style='background:#fff; padding:30px; border-radius:10px; max-width:500px; margin:0 auto; border-top: 5px solid #e74c3c;'> 395 <h2 style='color:#e74c3c; margin-top:0;'>🚨 Security Alert</h2> 396 <p style='color:#666;'> 397 <strong>Site:</strong> " . get_bloginfo('name') . "<br> 398 <strong>User:</strong> " . esc_html($user->user_login) . "<br> 399 <strong>Alert:</strong> " . esc_html($alert_type) . "<br> 400 <strong>IP Address:</strong> " . esc_html($ip) . "<br> 401 <strong>Time:</strong> " . current_time('mysql') . " 402 </p> 403 <div style='background:#fee; border-left:4px solid #e74c3c; padding:15px; margin:20px 0;'> 404 <strong>What should you do?</strong><br> 405 If this wasn't you, please change your password immediately and contact your site administrator. 406 </div> 407 <p style='color:#999; font-size:12px;'>Secured by BaseCloud Shield</p> 408 </div> 409 </div>"; 410 } 411 210 412 // --- 3. DELIVERY METHODS --- 211 413 … … 326 528 <?php endif; ?> 327 529 <form method="post"> 328 <input type="text" name="otp_input" autocomplete="off" autofocus required placeholder="000000" maxlength="6" /> 530 <?php wp_nonce_field('bcshield_verify_otp', 'bcshield_nonce'); ?> 531 <input type="text" name="otp_input" autocomplete="off" autofocus required placeholder="000000" maxlength="6" pattern="[0-9]{6}" inputmode="numeric" /> 329 532 <button type="submit" name="bcshield_otp_submit">Verify & Login</button> 330 533 </form> … … 339 542 340 543 private function validate_otp() { 341 if (!isset($_COOKIE['bcshield_pending_user']) ) {544 if (!isset($_COOKIE['bcshield_pending_user']) || !isset($_COOKIE['bcshield_session'])) { 342 545 wp_redirect(home_url()); 343 546 exit; … … 345 548 346 549 $user_id = intval($_COOKIE['bcshield_pending_user']); 347 $correct_otp = get_transient('bcshield_otp_' . $user_id); 550 $client_ip = $this->get_client_ip(); 551 $user_agent = $this->get_user_agent(); 552 553 // Verify CSRF nonce 554 if (!isset($_POST['bcshield_nonce']) || !wp_verify_nonce($_POST['bcshield_nonce'], 'bcshield_verify_otp')) { 555 $this->log_security_event('csrf_attempt', $user_id, $client_ip, 'CSRF token validation failed'); 556 wp_redirect(home_url()); 557 exit; 558 } 559 560 // Verify session token to prevent session fixation 561 $expected_session = $this->generate_session_token($user_id, $client_ip, $user_agent); 562 if (!hash_equals($expected_session, $_COOKIE['bcshield_session'])) { 563 $this->log_security_event('session_mismatch', $user_id, $client_ip, 'Session token mismatch detected'); 564 $this->send_security_alert(get_userdata($user_id), 'Suspicious session detected', $client_ip); 565 wp_redirect(home_url()); 566 exit; 567 } 568 569 $otp_data = get_transient('bcshield_otp_' . $user_id); 348 570 $user_input = sanitize_text_field($_POST['otp_input']); 349 350 if ($correct_otp && $user_input == $correct_otp) { 351 setcookie('bcshield_2fa_verified', md5($user_id . 'bcshield_trust'), time() + (12 * 3600), '/'); 571 572 if (!$otp_data) { 573 $this->otp_error = 'Code expired. Please login again.'; 574 $this->log_security_event('otp_expired', $user_id, $client_ip, 'Attempted to use expired OTP'); 575 return; 576 } 577 578 // Check if too many attempts 579 if ($otp_data['attempts'] >= BCSHIELD_MAX_ATTEMPTS) { 352 580 delete_transient('bcshield_otp_' . $user_id); 353 setcookie('bcshield_pending_user', '', time() - 3600, '/'); 581 $this->lock_out_ip($client_ip); 582 $this->log_security_event('max_attempts', $user_id, $client_ip, 'Maximum OTP attempts exceeded'); 583 $this->send_security_alert(get_userdata($user_id), 'Multiple failed OTP attempts', $client_ip); 584 $this->otp_error = 'Too many failed attempts. Account temporarily locked.'; 585 return; 586 } 587 588 // Verify IP and User Agent match (prevent OTP interception) 589 if ($otp_data['ip'] !== $client_ip) { 590 $this->log_security_event('ip_mismatch', $user_id, $client_ip, 'OTP verification from different IP: ' . $otp_data['ip']); 591 $this->send_security_alert(get_userdata($user_id), 'OTP accessed from different IP', $client_ip); 592 $this->otp_error = 'Security validation failed. Please login again.'; 593 delete_transient('bcshield_otp_' . $user_id); 594 return; 595 } 596 597 if (hash_equals((string)$otp_data['code'], $user_input)) { 598 // Success - Create trusted device token 599 $trust_hash = $this->generate_trust_hash($user_id, $client_ip, $user_agent); 600 setcookie('bcshield_2fa_verified', $trust_hash, time() + (12 * 3600), '/', '', true, true); 601 602 delete_transient('bcshield_otp_' . $user_id); 603 setcookie('bcshield_pending_user', '', time() - 3600, '/', '', true, true); 604 setcookie('bcshield_session', '', time() - 3600, '/', '', true, true); 605 606 $this->log_security_event('otp_success', $user_id, $client_ip, 'OTP verified successfully'); 607 354 608 wp_set_auth_cookie($user_id); 355 609 wp_redirect(admin_url()); 356 610 exit; 357 611 } else { 358 $this->otp_error = 'Incorrect Code. Please try again.'; 612 // Failed attempt - increment counter 613 $otp_data['attempts']++; 614 $validity_remaining = get_option('_transient_timeout_bcshield_otp_' . $user_id) - time(); 615 set_transient('bcshield_otp_' . $user_id, $otp_data, $validity_remaining); 616 617 $remaining_attempts = BCSHIELD_MAX_ATTEMPTS - $otp_data['attempts']; 618 $this->log_security_event('otp_failed', $user_id, $client_ip, 'Failed OTP attempt ' . $otp_data['attempts']); 619 620 $this->otp_error = 'Incorrect code. ' . $remaining_attempts . ' attempt(s) remaining.'; 359 621 } 360 622 } -
basecloud-shield/trunk/package.json
r3442419 r3452601 1 1 { 2 2 "name": "basecloud-shield", 3 "version": "1.2. 4",3 "version": "1.2.7", 4 4 "description": "WordPress 2FA Security Plugin - Build and deployment scripts", 5 5 "scripts": { -
basecloud-shield/trunk/readme.txt
r3442655 r3452601 4 4 Requires at least: 5.0 5 5 Tested up to: 6.9 6 Stable tag: 1.2. 66 Stable tag: 1.2.7 7 7 Requires PHP: 7.4 8 8 License: GPLv2 or later … … 118 118 119 119 == Changelog == 120 121 = 1.2.7 = 122 **Critical Security & Bug Fix Release** 123 124 **CRITICAL FIX - Duplicate OTP Prevention:** 125 • Fixed issue causing multiple duplicate OTP emails to be sent 126 • Implemented email deduplication across all delivery methods 127 • Added phone number deduplication for WhatsApp/SMS 128 • Enhanced recipient list processing to prevent duplicate entries 129 • Added 60-second OTP generation lock to prevent rapid duplicates 130 131 **Enterprise-Grade Security Enhancements:** 132 • Brute Force Protection: Maximum 5 OTP attempts before 15-minute IP lockout 133 • Rate Limiting: 3 OTP requests per 10-minute window per user/IP 134 • Cryptographically Secure OTP: Replaced rand() with random_bytes() 135 • Session Binding: IP address validation, User-Agent fingerprinting 136 • HMAC-SHA256 session tokens to prevent session fixation attacks 137 • CSRF Protection: WordPress nonce validation on all OTP submissions 138 • Enhanced Cookie Security: httponly and secure flags on all cookies 139 • Security Event Logging: Comprehensive audit trail (last 100 events) 140 • Real-Time Security Alerts: Email/webhook alerts for suspicious activity 141 • Timing Attack Protection: Constant-time comparisons using hash_equals() 142 143 **Attack Prevention:** 144 • OTP Interception Prevention (IP binding) 145 • Session Hijacking Detection (multi-factor validation) 146 • CSRF Attack Protection (nonce tokens) 147 • Replay Attack Prevention (one-time codes with metadata) 148 • Rate Limit Abuse Prevention (throttling) 149 • Brute Force Attack Blocking (auto-lockout) 150 151 **Security Monitoring:** 152 • 12 new security event types tracked and logged 153 • IP mismatch detection and alerting 154 • Session token mismatch detection 155 • Failed attempt tracking with remaining attempt counter 156 • Expired OTP usage attempt logging 157 • Invalid trust cookie detection 158 159 **Technical Improvements:** 160 • Enhanced IP detection (proxy, CloudFlare, load balancer support) 161 • OTP metadata tracking (IP, User-Agent, timestamp, attempts) 162 • Improved error messages with security context 163 • Pattern validation for numeric OTP input 164 • Better cookie management with expiration handling 120 165 121 166 = 1.2.6 =
Note: See TracChangeset
for help on using the changeset viewer.