Changeset 3492916
- Timestamp:
- 03/27/2026 06:40:02 PM (14 hours ago)
- Location:
- block-logins-cf/trunk
- Files:
-
- 2 edited
-
block-logins-cf.php (modified) (29 diffs)
-
readme.txt (modified) (7 diffs)
Legend:
- Unmodified
- Added
- Removed
-
block-logins-cf/trunk/block-logins-cf.php
r3489027 r3492916 4 4 * Plugin URI: https://github.com/supersoju/block-logins-cf 5 5 * Description: Blocks failed login attempts directly through Cloudflare. 6 * Version: 1. 06 * Version: 1.1 7 7 * Author: supersoju 8 8 * Author URI: https://supersoju.com … … 142 142 // Check if IP should be blocked 143 143 if ($failed_attempts >= $max_attempts) { 144 $auto_unblock_expiry = intval($settings['auto_unblock_hours'] ?? 24) * HOUR_IN_SECONDS; 145 set_transient("cfblocklogins_block_source_$ip", 'login', $auto_unblock_expiry); 144 146 cfblocklogins_block_ip($ip); 145 147 cfblocklogins_log_error("IP blocked after reaching attempt threshold", [ … … 167 169 function cfblocklogins_increment_failed_attempts_atomic($target, $settings, $type = 'ip') { 168 170 $lock_key = "cfblocklogins_lock_{$type}_{$target}"; 169 $attempt_key = "cfblocklogins_failed_login" . ($type === 'subnet' ? '_subnet' : '') . "_{$target}"; 171 if ($type === 'subnet') { 172 $type_suffix = '_subnet'; 173 } elseif ($type === 'xmlrpc') { 174 $type_suffix = '_xmlrpc'; 175 } elseif ($type === 'notfound') { 176 $type_suffix = '_notfound'; 177 } else { 178 $type_suffix = ''; 179 } 180 $attempt_key = "cfblocklogins_failed_login{$type_suffix}_{$target}"; 170 181 $block_duration = intval($settings['block_duration'] ?? 60); 171 182 $lock_timeout = 5; // 5 second lock timeout … … 359 370 360 371 cfblocklogins_log_error("Successfully blocked subnet via Cloudflare", ['subnet' => $subnet, 'rule_id' => $api_result['result']['id'] ?? 'unknown']); 372 return true; 373 } 374 375 /** 376 * Fetch blocked IPs from Cloudflare that were created by this plugin. 377 * 378 * @return array{results: array, capped: bool}|WP_Error 379 */ 380 function cfblocklogins_fetch_cf_blocked_ips() { 381 $credentials = cfblocklogins_get_api_credentials(); 382 $email = $credentials['email'] ?? ''; 383 $api_key = $credentials['api_key'] ?? ''; 384 $zone_id = $credentials['zone_id'] ?? ''; 385 386 if (!$email || !$api_key || !$zone_id) { 387 return new WP_Error( 388 'missing_credentials', 389 __('Cloudflare credentials are not configured. Please check your settings.', 'block-logins-cf') 390 ); 391 } 392 393 $results = []; 394 $page = 1; 395 $max_pages = 5; 396 $capped = false; 397 398 while ($page <= $max_pages) { 399 $url = "https://api.cloudflare.com/client/v4/zones/{$zone_id}/firewall/access_rules/rules?mode=block&per_page=100&page={$page}"; 400 $response = wp_remote_get($url, [ 401 'headers' => [ 402 'X-Auth-Email' => $email, 403 'X-Auth-Key' => $api_key, 404 'Content-Type' => 'application/json', 405 ], 406 'timeout' => 15, 407 ]); 408 409 $data = cfblocklogins_validate_api_response($response, "fetch_cf_blocked_ips:page_{$page}"); 410 if ($data === false) { 411 return new WP_Error( 412 'api_error', 413 __('Failed to retrieve blocked IPs from Cloudflare. Please check your credentials and try again.', 'block-logins-cf') 414 ); 415 } 416 417 $rules = $data['result'] ?? []; 418 if (empty($rules)) { 419 break; 420 } 421 422 foreach ($rules as $rule) { 423 $notes = sanitize_text_field($rule['notes'] ?? ''); 424 if (strpos($notes, 'Auto-generated by Block Logins CF plugin') === false) { 425 continue; 426 } 427 $config = $rule['configuration'] ?? []; 428 $ip_value = $config['value'] ?? ''; 429 $target = $config['target'] ?? 'ip'; 430 if (!$ip_value) { 431 continue; 432 } 433 $results[] = [ 434 'value' => $ip_value, 435 'target' => $target, 436 'rule_id' => $rule['id'] ?? '', 437 'notes' => $notes, 438 ]; 439 } 440 441 $total_pages = intval($data['result_info']['total_pages'] ?? 1); 442 if ($page >= $total_pages) { 443 break; 444 } 445 446 $page++; 447 } 448 449 if ($page > $max_pages) { 450 $capped = true; 451 } 452 453 return ['results' => $results, 'capped' => $capped]; 454 } 455 456 /** 457 * Create local transients for a blocked IP/subnet without calling the Cloudflare API. 458 * Used when importing rules that already exist in Cloudflare. 459 * 460 * @param string $ip IP address or CIDR subnet. 461 * @return bool True on success, false if $ip fails validation. 462 */ 463 function cfblocklogins_create_local_block($ip) { 464 if (!cfblocklogins_validate_ip_or_cidr($ip)) { 465 return false; 466 } 467 468 $settings = get_option('cfblocklogins_settings', []); 469 $auto_unblock_hours = intval($settings['auto_unblock_hours'] ?? 24); 470 $auto_unblock_seconds = $auto_unblock_hours * 3600; 471 472 set_transient("cfblocklogins_block_login_$ip", '1', $auto_unblock_seconds); 473 set_transient("cfblocklogins_block_login_time_$ip", time(), $auto_unblock_seconds); 474 475 wp_cache_delete('cfblocklogins_blocked_ip_transients', 'block-logins-cf'); 476 wp_cache_delete('cfblocklogins_blocked_ip_transients_cron', 'block-logins-cf'); 477 361 478 return true; 362 479 } … … 476 593 ]); 477 594 595 // Clear cached token validation so the next page load re-checks with the new token 596 $cache_key = 'cfblocklogins_token_valid_' . substr(md5($input['api_key']), 0, 8); 597 delete_transient($cache_key); 598 478 599 // Encrypt sensitive credentials before saving 479 600 return cfblocklogins_encrypt_api_credentials($new_settings); … … 490 611 'xmlrpc_max_attempts' => intval($input['xmlrpc_max_attempts'] ?? ($current['xmlrpc_max_attempts'] ?? 3)), 491 612 'xmlrpc_block_duration' => intval($input['xmlrpc_block_duration'] ?? ($current['xmlrpc_block_duration'] ?? 300)), 613 'enable_404_blocking' => !empty($input['enable_404_blocking']) ? 1 : 0, 614 '404_max_attempts' => max(1, intval($input['404_max_attempts'] ?? ($current['404_max_attempts'] ?? 20))), 615 '404_time_window' => max(60, intval($input['404_time_window'] ?? ($current['404_time_window'] ?? 300))), 492 616 'whitelist' => $input['whitelist'], // preserve whitelist 493 617 ]); … … 519 643 xmlrpcCheckbox.addEventListener('change', function() { 520 644 xmlrpcRow.style.display = this.checked ? '' : 'none'; 645 }); 646 } 647 648 var notfoundCheckbox = document.getElementById('enable_404_blocking'); 649 var notfoundRow = document.getElementById('notfound-settings-row'); 650 if (notfoundCheckbox && notfoundRow) { 651 notfoundCheckbox.addEventListener('change', function() { 652 notfoundRow.style.display = this.checked ? '' : 'none'; 521 653 }); 522 654 } … … 648 780 </td> 649 781 </tr> 782 <tr valign="top"> 783 <th scope="row" colspan="2"><strong><?php esc_html_e('404 Scan Protection', 'block-logins-cf'); ?></strong></th> 784 </tr> 785 <tr valign="top"> 786 <th scope="row"><?php esc_html_e('Enable 404 Blocking', 'block-logins-cf'); ?></th> 787 <td> 788 <label> 789 <input type="checkbox" id="enable_404_blocking" name="cfblocklogins_settings[enable_404_blocking]" value="1" <?php checked(!empty($options['enable_404_blocking'])); ?> /> 790 <?php esc_html_e('Block IPs that generate too many 404 responses.', 'block-logins-cf'); ?> 791 </label> 792 <p class="description"> 793 <?php esc_html_e('Note: Automattic services (WordPress.com, Jetpack, VaultPress) and logged-in users are automatically excluded.', 'block-logins-cf'); ?> 794 </p> 795 </td> 796 </tr> 797 <tr valign="top" id="notfound-settings-row" <?php if (empty($options['enable_404_blocking'])) echo 'style="display:none;"'; ?>> 798 <th scope="row"><?php esc_html_e('404 Block Settings', 'block-logins-cf'); ?></th> 799 <td> 800 <p> 801 <?php esc_html_e('Block after', 'block-logins-cf'); ?> 802 <input type="number" min="1" style="width:70px;" name="cfblocklogins_settings[404_max_attempts]" value="<?php echo esc_attr($options['404_max_attempts'] ?? 20); ?>" /> 803 <?php esc_html_e('404 responses within', 'block-logins-cf'); ?> 804 <input type="number" min="60" style="width:90px;" name="cfblocklogins_settings[404_time_window]" value="<?php echo esc_attr($options['404_time_window'] ?? 300); ?>" /> 805 <?php esc_html_e('seconds', 'block-logins-cf'); ?> 806 </p> 807 <p class="description"> 808 <?php esc_html_e('404 blocking uses separate thresholds from regular login blocking.', 'block-logins-cf'); ?> 809 </p> 810 </td> 811 </tr> 650 812 </table> 651 813 <?php submit_button(esc_html__('Save Settings', 'block-logins-cf')); ?> … … 655 817 <h2><?php esc_html_e('Cloudflare API Credentials', 'block-logins-cf'); ?></h2> 656 818 <?php 657 // Validate credentials again for display 819 // Validate credentials again for display (cached for 5 minutes to avoid throttling) 658 820 $valid = false; 659 821 $debug = ''; 660 if (!empty($options['api_key'])) { 661 $url = "https://api.cloudflare.com/client/v4/user/tokens/verify"; 662 $response = wp_remote_get($url, [ 663 'headers' => [ 664 'Authorization' => 'Bearer ' . $options['api_key'], 665 'Content-Type' => 'application/json', 666 ], 667 'timeout' => 10, 668 ]); 669 670 $api_result = cfblocklogins_validate_api_response($response, 'token_verify_display'); 671 $valid = ($api_result !== false); 672 if (!$valid) { 673 $body = wp_remote_retrieve_body($response); 674 $debug_msg = is_wp_error($response) ? $response->get_error_message() : substr($body, 0, 200); 675 $debug = '<pre>' . esc_html($debug_msg) . '</pre>'; 822 if (!empty($decrypted_options['api_key'])) { 823 $cache_key = 'cfblocklogins_token_valid_' . substr(md5($decrypted_options['api_key']), 0, 8); 824 $cached = get_transient($cache_key); 825 if ($cached !== false) { 826 $valid = ( 'valid' === $cached ); 827 } else { 828 $url = "https://api.cloudflare.com/client/v4/user/tokens/verify"; 829 $response = wp_remote_get($url, [ 830 'headers' => [ 831 'Authorization' => 'Bearer ' . $decrypted_options['api_key'], 832 'Content-Type' => 'application/json', 833 ], 834 'timeout' => 10, 835 ]); 836 837 $api_result = cfblocklogins_validate_api_response($response, 'token_verify_display'); 838 $valid = ($api_result !== false); 839 set_transient($cache_key, $valid ? 'valid' : 'invalid', 5 * MINUTE_IN_SECONDS); 840 if (!$valid) { 841 $body = wp_remote_retrieve_body($response); 842 $debug_msg = is_wp_error($response) ? $response->get_error_message() : substr($body, 0, 200); 843 $debug = '<pre>' . esc_html($debug_msg) . '</pre>'; 844 } 676 845 } 677 846 } … … 708 877 $settings['last_whitelist_update'] = time(); // Optional: track last update time 709 878 $settings['whitelist'][] = $ip; 710 $result =update_option('cfblocklogins_settings', $settings);879 update_option('cfblocklogins_settings', $settings); 711 880 wp_cache_delete('cfblocklogins_settings', 'options'); 712 881 } … … 717 886 if (isset($settings['whitelist']) && is_array($settings['whitelist'])) { 718 887 $settings['whitelist'] = array_diff($settings['whitelist'], [$ip]); 719 $result =update_option('cfblocklogins_settings', $settings);888 update_option('cfblocklogins_settings', $settings); 720 889 wp_cache_delete('cfblocklogins_settings', 'options'); 721 890 } … … 727 896 wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'block-logins-cf')); 728 897 } 898 899 $cf_only_ips = null; 900 $cf_sync_error = null; 901 $cf_sync_capped = false; 729 902 730 903 // Handle unblock … … 746 919 } 747 920 $ip = sanitize_text_field(wp_unslash($_POST['cfblocklogins_whitelist_ip'])); 748 if ( filter_var($ip, FILTER_VALIDATE_IP)) {921 if (cfblocklogins_validate_ip_or_cidr($ip)) { 749 922 cfblocklogins_add_to_whitelist($ip); 750 echo '<div class="updated"><p>Whitelisted IP: ' . esc_html($ip) . '</p></div>';923 echo '<div class="updated"><p>Whitelisted: ' . esc_html($ip) . '</p></div>'; 751 924 } else { 752 echo '<div class="error"><p>Invalid IP address .</p></div>';925 echo '<div class="error"><p>Invalid IP address or CIDR notation.</p></div>'; 753 926 } 754 927 } … … 777 950 } 778 951 952 // Handle Cloudflare sync fetch 953 if (isset($_POST['cfblocklogins_fetch_cf_ips']) && check_admin_referer('cfblocklogins_fetch_cf_ips_action')) { 954 if (!current_user_can('manage_options')) { 955 wp_die(esc_html__('You do not have sufficient permissions to perform this action.', 'block-logins-cf')); 956 } 957 $fetch_result = cfblocklogins_fetch_cf_blocked_ips(); 958 if (is_wp_error($fetch_result)) { 959 $cf_sync_error = $fetch_result->get_error_message(); 960 } else { 961 $cf_sync_capped = $fetch_result['capped']; 962 $cf_only_ips = []; 963 foreach ($fetch_result['results'] as $rule) { 964 $ip = sanitize_text_field($rule['value']); 965 if (!cfblocklogins_validate_ip_or_cidr($ip)) { 966 continue; // Skip malformed rules 967 } 968 if (!get_transient("cfblocklogins_block_login_$ip")) { 969 $cf_only_ips[] = $rule; 970 } 971 } 972 } 973 } 974 975 // Handle single IP import from Cloudflare 976 if (isset($_POST['cfblocklogins_import_cf_ip']) && check_admin_referer('cfblocklogins_import_cf_ip_action')) { 977 if (!current_user_can('manage_options')) { 978 wp_die(esc_html__('You do not have sufficient permissions to perform this action.', 'block-logins-cf')); 979 } 980 $ip = sanitize_text_field(wp_unslash($_POST['cfblocklogins_import_ip'] ?? '')); 981 if (cfblocklogins_create_local_block($ip)) { 982 wp_safe_redirect(admin_url('admin.php?page=block-logins-cf-blocked')); 983 } else { 984 wp_safe_redirect(admin_url('admin.php?page=block-logins-cf-blocked&cf_import_error=1')); 985 } 986 exit; 987 } 988 989 // Handle import all from Cloudflare 990 if (isset($_POST['cfblocklogins_import_all_cf_ips']) && check_admin_referer('cfblocklogins_import_all_cf_ips_action')) { 991 if (!current_user_can('manage_options')) { 992 wp_die(esc_html__('You do not have sufficient permissions to perform this action.', 'block-logins-cf')); 993 } 994 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized in loop below 995 $raw_ips = isset($_POST['cfblocklogins_import_ips']) ? (array) wp_unslash($_POST['cfblocklogins_import_ips']) : []; 996 foreach ($raw_ips as $ip) { 997 cfblocklogins_create_local_block(sanitize_text_field($ip)); 998 } 999 wp_safe_redirect(admin_url('admin.php?page=block-logins-cf-blocked')); 1000 exit; 1001 } 1002 779 1003 // Find blocked IPs (transients) 780 1004 global $wpdb; … … 788 1012 $wpdb->prepare( 789 1013 "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", 790 '_transient_cf _block_login_%'1014 '_transient_cfblocklogins_block_login_%' 791 1015 ) 792 1016 ); … … 796 1020 foreach ($transients as $transient) { 797 1021 $name = $transient->option_name; 798 if (strpos($name, '_transient_cf _block_login_time_') === 0) {1022 if (strpos($name, '_transient_cfblocklogins_block_login_time_') === 0) { 799 1023 continue; // skip time transients 800 1024 } 801 $ip = str_replace('_transient_cf _block_login_', '', $name);1025 $ip = str_replace('_transient_cfblocklogins_block_login_', '', $name); 802 1026 $blocked_ips[] = $ip; 803 1027 } … … 808 1032 <h1><?php esc_html_e('Blocked and Whitelisted IPs', 'block-logins-cf'); ?></h1> 809 1033 <h2><?php esc_html_e('Currently Blocked', 'block-logins-cf'); ?></h2> 1034 <form method="post" style="margin-bottom:1em;"> 1035 <?php wp_nonce_field('cfblocklogins_fetch_cf_ips_action'); ?> 1036 <input type="submit" name="cfblocklogins_fetch_cf_ips" class="button" value="<?php esc_attr_e('Sync from Cloudflare', 'block-logins-cf'); ?>"> 1037 </form> 1038 <?php if (isset($_GET['cf_import_error']) && sanitize_text_field(wp_unslash($_GET['cf_import_error'])) === '1'): ?> 1039 <div class="notice notice-error"><p><?php esc_html_e('Failed to import IP: invalid IP address or CIDR notation.', 'block-logins-cf'); ?></p></div> 1040 <?php elseif ($cf_sync_error !== null): ?> 1041 <div class="notice notice-error"><p><?php echo esc_html($cf_sync_error); ?></p></div> 1042 <?php elseif ($cf_only_ips !== null && empty($cf_only_ips)): ?> 1043 <div class="notice notice-success"><p><?php esc_html_e('No untracked IPs found in Cloudflare.', 'block-logins-cf'); ?></p></div> 1044 <?php endif; ?> 810 1045 <table class="widefat"> 811 1046 <thead> 812 1047 <tr> 813 1048 <th><?php esc_html_e('IP Address', 'block-logins-cf'); ?></th> 1049 <th><?php esc_html_e('Source', 'block-logins-cf'); ?></th> 814 1050 <th><?php esc_html_e('Time Until Unblock', 'block-logins-cf'); ?></th> 815 1051 <th><?php esc_html_e('Action', 'block-logins-cf'); ?></th> … … 818 1054 <tbody> 819 1055 <?php if (empty($blocked_ips)): ?> 820 <tr><td colspan=" 3"><?php esc_html_e('No blocked IPs.', 'block-logins-cf'); ?></td></tr>1056 <tr><td colspan="4"><?php esc_html_e('No blocked IPs.', 'block-logins-cf'); ?></td></tr> 821 1057 <?php else: foreach ($blocked_ips as $ip): ?> 822 1058 <tr> 823 1059 <td><?php echo esc_html($ip); ?></td> 1060 <td> 1061 <?php 1062 $source = get_transient("cfblocklogins_block_source_$ip"); 1063 $source_labels = [ 1064 'notfound' => '<span style="background:#d63638;color:#fff;padding:2px 6px;border-radius:3px;font-size:11px;">404 Scan</span>', 1065 'xmlrpc' => '<span style="background:#2271b1;color:#fff;padding:2px 6px;border-radius:3px;font-size:11px;">XML-RPC</span>', 1066 'login' => '<span style="background:#1d2327;color:#fff;padding:2px 6px;border-radius:3px;font-size:11px;">Login</span>', 1067 ]; 1068 echo wp_kses($source_labels[$source] ?? $source_labels['login'], ['span' => ['style' => []]]); 1069 ?> 1070 </td> 824 1071 <td> 825 1072 <?php … … 854 1101 </tbody> 855 1102 </table> 856 1103 1104 <?php if ($cf_only_ips !== null && !empty($cf_only_ips)): ?> 1105 <h2><?php esc_html_e('Cloudflare-only IPs', 'block-logins-cf'); ?></h2> 1106 <p><?php esc_html_e('These IPs are blocked in Cloudflare by this plugin but not tracked locally. Import them to sync.', 'block-logins-cf'); ?></p> 1107 <?php if ($cf_sync_capped): ?> 1108 <div class="notice notice-warning inline"><p><?php esc_html_e('Showing first 500 rules. Additional rules may exist in Cloudflare.', 'block-logins-cf'); ?></p></div> 1109 <?php endif; ?> 1110 <form method="post" style="margin-bottom:1em;"> 1111 <?php wp_nonce_field('cfblocklogins_import_all_cf_ips_action'); ?> 1112 <?php foreach ($cf_only_ips as $rule): ?> 1113 <input type="hidden" name="cfblocklogins_import_ips[]" value="<?php echo esc_attr($rule['value']); ?>"> 1114 <?php endforeach; ?> 1115 <input type="submit" name="cfblocklogins_import_all_cf_ips" class="button button-primary" value="<?php esc_attr_e('Import All', 'block-logins-cf'); ?>"> 1116 </form> 1117 <table class="widefat"> 1118 <thead> 1119 <tr> 1120 <th><?php esc_html_e('IP Address', 'block-logins-cf'); ?></th> 1121 <th><?php esc_html_e('Notes', 'block-logins-cf'); ?></th> 1122 <th><?php esc_html_e('Action', 'block-logins-cf'); ?></th> 1123 </tr> 1124 </thead> 1125 <tbody> 1126 <?php foreach ($cf_only_ips as $rule): ?> 1127 <tr> 1128 <td><?php echo esc_html($rule['value']); ?></td> 1129 <td><?php echo esc_html($rule['notes']); ?></td> 1130 <td> 1131 <form method="post" style="display:inline;"> 1132 <?php wp_nonce_field('cfblocklogins_import_cf_ip_action'); ?> 1133 <input type="hidden" name="cfblocklogins_import_ip" value="<?php echo esc_attr($rule['value']); ?>"> 1134 <input type="submit" name="cfblocklogins_import_cf_ip" class="button" value="<?php esc_attr_e('Import', 'block-logins-cf'); ?>"> 1135 </form> 1136 </td> 1137 </tr> 1138 <?php endforeach; ?> 1139 </tbody> 1140 </table> 1141 <?php endif; ?> 1142 857 1143 <h3><?php esc_html_e('Manually Block an IP', 'block-logins-cf'); ?></h3> 858 1144 <form method="post"> … … 889 1175 <form method="post" style="margin-bottom:1em;"> 890 1176 <?php wp_nonce_field('cfblocklogins_whitelist_ip_action'); ?> 891 <input type="text" name="cfblocklogins_whitelist_ip" placeholder="<?php esc_attr_e(' Enter IP address', 'block-logins-cf'); ?>" required>1177 <input type="text" name="cfblocklogins_whitelist_ip" placeholder="<?php esc_attr_e('IP or CIDR (e.g., 192.168.1.0/24)', 'block-logins-cf'); ?>" required> 892 1178 <input type="submit" class="button" value="<?php esc_attr_e('Add to Whitelist', 'block-logins-cf'); ?>"> 893 1179 </form> 1180 1181 <?php 1182 $settings = get_option('cfblocklogins_settings', []); 1183 if (!empty($settings['enable_404_blocking'])): 1184 $today = gmdate('Y-m-d'); 1185 $log_404 = get_transient("cfblocklogins_404_access_$today"); 1186 ?> 1187 <hr> 1188 <h2><?php esc_html_e('404 Activity Today', 'block-logins-cf'); ?></h2> 1189 <?php if (empty($log_404)): ?> 1190 <p><?php esc_html_e('No 404 activity recorded today.', 'block-logins-cf'); ?></p> 1191 <?php else: ?> 1192 <table class="widefat" style="margin-bottom:1em;max-width:500px;"> 1193 <tbody> 1194 <tr> 1195 <th><?php esc_html_e('Total requests today', 'block-logins-cf'); ?></th> 1196 <td><?php echo esc_html($log_404['total_requests']); ?></td> 1197 </tr> 1198 <tr> 1199 <th><?php esc_html_e('Unique IPs', 'block-logins-cf'); ?></th> 1200 <td><?php echo esc_html(count($log_404['unique_ips'])); ?></td> 1201 </tr> 1202 <tr> 1203 <th><?php esc_html_e('Top missing paths', 'block-logins-cf'); ?></th> 1204 <td> 1205 <?php foreach ($log_404['top_paths'] as $path => $count): ?> 1206 <code><?php echo esc_html($path); ?></code> (<?php echo esc_html($count); ?>)<br> 1207 <?php endforeach; ?> 1208 </td> 1209 </tr> 1210 </tbody> 1211 </table> 1212 1213 <?php if (!empty($log_404['per_ip'])): ?> 1214 <h3><?php esc_html_e('Per-IP Breakdown', 'block-logins-cf'); ?></h3> 1215 <table class="widefat"> 1216 <thead> 1217 <tr> 1218 <th><?php esc_html_e('IP Address', 'block-logins-cf'); ?></th> 1219 <th><?php esc_html_e('Requests today', 'block-logins-cf'); ?></th> 1220 <th><?php esc_html_e('Threshold', 'block-logins-cf'); ?></th> 1221 <th><?php esc_html_e('Status', 'block-logins-cf'); ?></th> 1222 </tr> 1223 </thead> 1224 <tbody> 1225 <?php 1226 $max_attempts = intval($settings['404_max_attempts'] ?? 20); 1227 uasort($log_404['per_ip'], fn($a, $b) => $b['count'] <=> $a['count']); 1228 foreach ($log_404['per_ip'] as $per_ip_addr => $per_ip_data): 1229 $count = $per_ip_data['count']; 1230 $is_blocked = (bool) get_transient("cfblocklogins_block_login_$per_ip_addr"); 1231 if ($is_blocked) { 1232 $status = '<span style="color:#d63638;font-weight:bold;">Blocked</span>'; 1233 } elseif ($count >= $max_attempts * 0.75) { 1234 $status = '<span style="color:#d63638;">Approaching</span>'; 1235 } else { 1236 $status = '<span style="color:#00a32a;">Normal</span>'; 1237 } 1238 ?> 1239 <tr> 1240 <td><?php echo esc_html($per_ip_addr); ?></td> 1241 <td><?php echo esc_html($count); ?></td> 1242 <td><?php echo esc_html($max_attempts); ?></td> 1243 <td><?php echo wp_kses($status, ['span' => ['style' => []]]); ?></td> 1244 </tr> 1245 <?php endforeach; ?> 1246 </tbody> 1247 </table> 1248 <?php endif; ?> 1249 <?php endif; ?> 1250 <?php endif; ?> 894 1251 895 1252 </div> … … 904 1261 } 905 1262 // For IPv6 or invalid, return false or handle as needed 1263 return false; 1264 } 1265 1266 // Validate IP address or CIDR notation 1267 function cfblocklogins_validate_ip_or_cidr($input) { 1268 // Check if it's a plain IP address 1269 if (filter_var($input, FILTER_VALIDATE_IP)) { 1270 return true; 1271 } 1272 1273 // Check if it's CIDR notation 1274 if (strpos($input, '/') !== false) { 1275 list($ip, $bits) = explode('/', $input, 2); 1276 1277 // Validate the IP part 1278 if (!filter_var($ip, FILTER_VALIDATE_IP)) { 1279 return false; 1280 } 1281 1282 // Validate the prefix length 1283 $bits = intval($bits); 1284 if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { 1285 // IPv4: prefix must be 0-32 1286 return $bits >= 0 && $bits <= 32; 1287 } elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { 1288 // IPv6: prefix must be 0-128 1289 return $bits >= 0 && $bits <= 128; 1290 } 1291 } 1292 906 1293 return false; 907 1294 } … … 1029 1416 1030 1417 list($subnet, $bits) = explode('/', $range); 1418 $bits = intval($bits); 1031 1419 1032 1420 if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && … … 1040 1428 } 1041 1429 1042 // IPv6 support could be added here in the future 1430 // IPv6 CIDR check 1431 if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && 1432 filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { 1433 // Convert IPv6 addresses to binary strings 1434 $ip_bin = inet_pton($ip); 1435 $subnet_bin = inet_pton($subnet); 1436 1437 if ($ip_bin === false || $subnet_bin === false) { 1438 return false; 1439 } 1440 1441 // Build the netmask binary string 1442 $mask = str_repeat('f', intval($bits / 4)); 1443 $remainder = $bits % 4; 1444 if ($remainder > 0) { 1445 $mask .= dechex(0xf << (4 - $remainder) & 0xf); 1446 } 1447 $mask = str_pad($mask, 32, '0'); 1448 $mask_bin = pack('H*', $mask); 1449 1450 // Compare masked addresses 1451 return ($ip_bin & $mask_bin) === ($subnet_bin & $mask_bin); 1452 } 1453 1043 1454 return false; 1044 1455 } … … 1232 1643 $wpdb->prepare( 1233 1644 "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", 1234 '_transient_cf _block_login_%'1645 '_transient_cfblocklogins_block_login_%' 1235 1646 ) 1236 1647 ); … … 1240 1651 foreach ($transients as $transient) { 1241 1652 $name = $transient->option_name; 1242 if (strpos($name, '_transient_cf _block_login_time_') === 0) {1653 if (strpos($name, '_transient_cfblocklogins_block_login_time_') === 0) { 1243 1654 continue; // skip time transients 1244 1655 } 1245 $ip_or_subnet = str_replace('_transient_cf _block_login_', '', $name);1656 $ip_or_subnet = str_replace('_transient_cfblocklogins_block_login_', '', $name); 1246 1657 1247 1658 // Check if this transient should be expired … … 1369 1780 ]); 1370 1781 } 1782 1783 function cfblocklogins_log_404_access($ip) { 1784 $today = gmdate('Y-m-d'); 1785 $cache_key = "cfblocklogins_404_access_$today"; 1786 $expiry = 48 * HOUR_IN_SECONDS; 1787 1788 $log = get_transient($cache_key); 1789 if (!is_array($log)) { 1790 $log = [ 1791 'total_requests' => 0, 1792 'unique_ips' => [], 1793 'top_paths' => [], 1794 'per_ip' => [], 1795 'first_seen' => time(), 1796 'last_seen' => time(), 1797 ]; 1798 } 1799 1800 // Get request path (strip query string) 1801 $raw_uri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '/'; 1802 $path = explode('?', $raw_uri, 2)[0]; 1803 1804 $log['total_requests']++; 1805 $log['last_seen'] = time(); 1806 1807 // Track unique IPs (cap at 100) 1808 if (!in_array($ip, $log['unique_ips'], true) && count($log['unique_ips']) < 100) { 1809 $log['unique_ips'][] = $ip; 1810 } 1811 1812 // Track path counts (sort descending, keep top 10) 1813 $log['top_paths'][$path] = ($log['top_paths'][$path] ?? 0) + 1; 1814 arsort($log['top_paths']); 1815 $log['top_paths'] = array_slice($log['top_paths'], 0, 10, true); 1816 1817 // Track per-IP counts (cap at 100 entries) 1818 if (isset($log['per_ip'][$ip])) { 1819 $log['per_ip'][$ip]['count']++; 1820 $log['per_ip'][$ip]['last_seen'] = time(); 1821 } elseif (count($log['per_ip']) < 100) { 1822 $log['per_ip'][$ip] = ['count' => 1, 'last_seen' => time()]; 1823 } 1824 1825 set_transient($cache_key, $log, $expiry); 1826 } 1827 1828 function cfblocklogins_track_404() { 1829 if (!is_404()) { 1830 return; 1831 } 1832 1833 if (is_user_logged_in()) { 1834 return; 1835 } 1836 1837 if (wp_doing_ajax() || (defined('REST_REQUEST') && REST_REQUEST)) { 1838 return; 1839 } 1840 1841 $settings = get_option('cfblocklogins_settings', []); 1842 1843 if (empty($settings['enable_404_blocking'])) { 1844 return; 1845 } 1846 1847 $ip = cfblocklogins_get_user_ip(); 1848 1849 if (cfblocklogins_is_automattic_ip($ip)) { 1850 return; 1851 } 1852 1853 $whitelist = cfblocklogins_get_whitelist(); 1854 if (in_array($ip, $whitelist, true)) { 1855 return; 1856 } 1857 1858 if (cfblocklogins_check_if_blocked($ip)) { 1859 return; 1860 } 1861 1862 cfblocklogins_log_404_access($ip); 1863 1864 $max_attempts = max(1, intval($settings['404_max_attempts'] ?? 20)); 1865 $time_window = max(60, intval($settings['404_time_window'] ?? 300)); 1866 1867 $result = cfblocklogins_increment_failed_attempts_atomic($ip, [ 1868 'block_duration' => $time_window, 1869 'max_attempts' => $max_attempts, 1870 ], 'notfound'); 1871 1872 if ($result && $result['attempts'] >= $max_attempts) { 1873 $auto_unblock_expiry = intval($settings['auto_unblock_hours'] ?? 24) * HOUR_IN_SECONDS; 1874 set_transient("cfblocklogins_block_source_$ip", 'notfound', $auto_unblock_expiry); 1875 cfblocklogins_block_ip($ip); 1876 cfblocklogins_log_error('IP blocked for 404 scanning', [ 1877 'ip' => $ip, 1878 'attempts' => $result['attempts'], 1879 'threshold'=> $max_attempts, 1880 ]); 1881 } 1882 } 1883 add_action('template_redirect', 'cfblocklogins_track_404', 1); 1371 1884 1372 1885 // Check if XML-RPC traffic suggests an attack … … 1490 2003 delete_transient("cfblocklogins_xmlrpc_notification_shown_$today"); 1491 2004 1492 wp_ redirect(admin_url('admin.php?page=block-logins-cf&xmlrpc_enabled=1'));2005 wp_safe_redirect(admin_url('admin.php?page=block-logins-cf&xmlrpc_enabled=1')); 1493 2006 exit; 1494 2007 } … … 1504 2017 delete_transient("cfblocklogins_xmlrpc_notification_shown_$today"); 1505 2018 1506 wp_ redirect(admin_url('admin.php?page=block-logins-cf&xmlrpc_dismissed=1'));2019 wp_safe_redirect(admin_url('admin.php?page=block-logins-cf&xmlrpc_dismissed=1')); 1507 2020 exit; 1508 2021 } … … 1551 2064 // Check if this attempt should trigger blocking 1552 2065 if ($attempts >= $max_attempts) { 2066 $auto_unblock_expiry = intval($settings['auto_unblock_hours'] ?? 24) * HOUR_IN_SECONDS; 2067 set_transient("cfblocklogins_block_source_$ip", 'xmlrpc', $auto_unblock_expiry); 1553 2068 // Block the IP using existing function 1554 2069 cfblocklogins_block_ip($ip); -
block-logins-cf/trunk/readme.txt
r3489027 r3492916 5 5 Tested up to: 7.0 6 6 Requires PHP: 7.4 7 Stable tag: 1. 07 Stable tag: 1.1 8 8 License: GPL-2.0-or-later 9 9 … … 15 15 16 16 - Block IPs via Cloudflare after X failed login attempts 17 - Block IPs that generate excessive 404 responses (bots and scanners) 18 - Block IPs attacking via XML-RPC with intelligent detection 17 19 - Automatic unblocking after a configurable duration 18 - Whitelist IPs to never block or track them 20 - Whitelist IPs to never block or track them (supports IPv6 CIDR ranges) 19 21 - View and manually unblock blocked IPs from the admin 22 - Block source tracking — see whether each IP was blocked via login, XML-RPC, or 404 23 - Sync existing Cloudflare blocks into the local blocked IPs list 20 24 - Secure settings page with Cloudflare API token validation 21 25 - Hourly cron job for automatic maintenance … … 26 30 27 31 **What is the Cloudflare API and what is it used for?** 28 The Cloudflare API is a RESTful service provided by Cloudflare, Inc. that allows programmatic management of Cloudflare firewall rules. This plugin uses it to automatically block and unblock IP addresses based on failed login attempts .32 The Cloudflare API is a RESTful service provided by Cloudflare, Inc. that allows programmatic management of Cloudflare firewall rules. This plugin uses it to automatically block and unblock IP addresses based on failed login attempts, XML-RPC attacks, and 404 scanning activity. 29 33 30 34 **What data is sent and when?** … … 35 39 - Endpoint: `https://api.cloudflare.com/client/v4/user/tokens/verify` 36 40 37 2. **When blocking an IP** (after failed loginthreshold is reached):41 2. **When blocking an IP** (after a threshold is reached): 38 42 - The IP address to be blocked 39 43 - Your Cloudflare email address and API key/token … … 42 46 - Endpoint: `https://api.cloudflare.com/client/v4/zones/{zone_id}/firewall/access_rules/rules` 43 47 44 No personally identifiable information about your WordPress users is transmitted. Only IP addresses of failed login attempts are sent to Cloudflare. 48 3. **When syncing from Cloudflare** (on demand): 49 - Fetches existing firewall rules from your Cloudflare zone 50 - Endpoint: `https://api.cloudflare.com/client/v4/zones/{zone_id}/firewall/access_rules/rules` 51 52 No personally identifiable information about your WordPress users is transmitted. Only IP addresses are sent to Cloudflare. 45 53 46 54 **Service provider information:** … … 71 79 This plugin blocks IPs at the Cloudflare firewall, stopping attacks before they reach your server. 72 80 81 = What does 404 blocking protect against? = 82 It detects bots and vulnerability scanners that probe your site by requesting many non-existent URLs. When an IP exceeds the configurable 404 threshold, it is blocked via Cloudflare just like a brute-force login attacker. 83 84 = Can I sync blocks I already have in Cloudflare? = 85 Yes. Use the "Sync from Cloudflare" button on the Blocked IPs page to import existing Cloudflare firewall rules into the plugin's local list. 86 87 == Screenshots == 88 89 1. **Settings Page:** Configure your Cloudflare credentials, blocking thresholds, and auto-unblock duration. 90 2. **Blocked IPs Management:** View currently blocked IPs with source tracking, unblock them, and manage your whitelist. 91 3. **Whitelist Management:** Add or remove IP addresses (including IPv6 CIDR ranges) from the whitelist. 92 73 93 == Changelog == 94 95 = 1.1 = 96 * Added 404-based IP blocking to detect and block bots and vulnerability scanners 97 * Added XML-RPC protection with intelligent attack detection 98 * Added block source tracking — blocked IPs now show whether they were blocked via login, XML-RPC, or 404 99 * Added 404 activity log in the Blocked IPs admin page 100 * Added "Sync from Cloudflare" to import existing Cloudflare firewall rules into the local list 101 * Added IPv6 CIDR range support in the IP whitelist 102 * Added caching for Cloudflare API token validation to prevent throttling 74 103 75 104 = 1.0 = … … 77 106 78 107 == Upgrade Notice == 108 109 = 1.1 = 110 Adds 404 blocking, XML-RPC protection, block source tracking, Cloudflare sync, and IPv6 CIDR whitelist support. 79 111 80 112 = 1.0 =
Note: See TracChangeset
for help on using the changeset viewer.