PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.8.1
Defender Security – Malware Scanner, Login Security & Firewall v5.8.1
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / src / component / class-blacklist-lockout.php

class-blacklist-lockout.php in Defender Security – Malware Scanner, Login Security & Firewall 5.8.1, at src/component/class-blacklist-lockout.php

521 lines 14.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles IP and country-based access restrictions.
4 *
5 * @package WP_Defender\Component
6 */
7
8 namespace WP_Defender\Component;
9
10 use PharData;
11 use WP_Defender\Component;
12 use WP_Defender\Traits\IP;
13 use Calotes\Helper\Array_Cache;
14 use WP_Defender\Traits\Country;
15 use WP_Defender\Traits\Continent;
16 use WP_Defender\Model\Lockout_Log;
17 use MaxMind\Db\Reader\InvalidDatabaseException;
18 use WP_Defender\Integrations\MaxMind_Geolocation;
19 use WP_Defender\Model\Setting\Blacklist_Lockout as Model_Blacklist_Lockout;
20 use WP_Defender\Integrations\Main_Wp;
21 use WP_Filesystem_Base;
22
23 /**
24 * Handles operations related to IP and country-based blacklisting and whitelisting.
25 */
26 class Blacklist_Lockout extends Component {
27
28 use Country;
29 use IP;
30 use Continent;
31
32 // Define the transient key.
33 const IP_LIST_KEY = 'wpmu_dev_ip_list';
34
35 /**
36 * Check if a country code belongs to a continent.
37 *
38 * @param string $country_iso The country ISO code to check.
39 * @param string $continent_code The continent code (EU, AS, AF, AM, OC).
40 * @return bool True if the country belongs to the continent, false otherwise.
41 */
42 private function is_country_in_continent( $country_iso, $continent_code ): bool {
43 $countries_with_continents = $this->get_countries_with_continents();
44
45 if ( ! isset( $countries_with_continents[ $continent_code ] ) ) {
46 return false;
47 }
48
49 $continent = $countries_with_continents[ $continent_code ];
50 if ( ! isset( $continent['area'] ) ) {
51 return false;
52 }
53
54 foreach ( $continent['area'] as $area ) {
55 if ( isset( $area['countries'][ $country_iso ] ) ) {
56 return true;
57 }
58 }
59
60 return false;
61 }
62
63 /**
64 * Checks if a given IP is whitelisted based on the country.
65 *
66 * @param string $ip The IP address to check.
67 *
68 * @return bool True if the IP is whitelisted based on the country, false otherwise.
69 * @throws InvalidDatabaseException Thrown for unexpected data is found in DB.
70 */
71 public function is_country_whitelist( $ip ): bool {
72 // Check Firewall > IP Banning > Locations section is activated or not.
73 $country = $this->get_current_country( $ip );
74 if ( false === $country ) {
75 return false;
76 }
77 $model = new Model_Blacklist_Lockout();
78 $whitelist = $model->get_country_whitelist();
79 if ( ! is_array( $whitelist ) || array() === $whitelist ) {
80 return false;
81 }
82
83 if ( isset( $country['iso'] ) && '' !== $country['iso'] ) {
84 $country_iso = strtoupper( $country['iso'] );
85
86 // Check if the specific country is in the whitelist.
87 if ( in_array( $country_iso, $whitelist, true ) ) {
88 return true;
89 }
90
91 // Check if any continent containing this country is in the whitelist.
92 foreach ( $whitelist as $allowed_code ) {
93 if ( $this->is_country_in_continent( $country_iso, $allowed_code ) ) {
94 return true;
95 }
96 }
97 }
98
99 return false;
100 }
101
102 /**
103 * Return the default ips need to whitelisted, e.g. HUB ips.
104 *
105 * @return array
106 */
107 private function get_default_ip_whitelisted(): array {
108 $server_addr = defender_get_data_from_request( 'SERVER_ADDR', 's' );
109 $remote_addr = defender_get_data_from_request( 'REMOTE_ADDR', 's' );
110 $ips = array(
111 ...$this->fetch_latest_ips(),
112 '127.0.0.1',
113 isset( $server_addr ) ? $server_addr : $remote_addr,
114 );
115
116 return (array) apply_filters( 'ip_lockout_default_whitelist_ip', $ips );
117 }
118
119 /**
120 * Fetch the latest IP list from the API.
121 *
122 * @return array List of IPs or an empty array on failure.
123 */
124 public static function fetch_latest_ips(): array {
125 // Attempt to retrieve IPs from the cache.
126 $cached_ips = get_site_transient( self::IP_LIST_KEY );
127 if ( is_array( $cached_ips ) && array() !== $cached_ips ) {
128 return $cached_ips;
129 }
130
131 // Fetch the IP list from the remote API.
132 $response = wp_remote_get(
133 'https://gist.githubusercontent.com/wpmu-docs/568114153e93eaf28f908a724b313b6f/raw',
134 array(
135 'timeout' => 10,
136 )
137 );
138
139 // Check for errors in the API response.
140 if ( is_wp_error( $response ) ) {
141 return array();
142 }
143
144 // Retrieve the response body.
145 $body = wp_remote_retrieve_body( $response );
146 if ( '' === $body ) {
147 return array();
148 }
149
150 // Process the response body into an array of IPs.
151 $ip_list = array_filter(
152 array_map(
153 'sanitize_text_field',
154 explode( "\n", $body )
155 ),
156 function ( $value ) {
157 return '' !== trim( $value );
158 }
159 );
160
161 // Cache the IP list for a week if it is not empty.
162 if ( array() !== $ip_list ) {
163 set_site_transient( self::IP_LIST_KEY, $ip_list, WEEK_IN_SECONDS );
164 }
165
166 // Return the fetched IP list.
167 return $ip_list;
168 }
169
170 /**
171 * Checks if an IP is whitelisted.
172 *
173 * @param string $ip The IP address to check.
174 *
175 * @return bool True if the IP is whitelisted, false otherwise.
176 */
177 public function is_ip_whitelisted( $ip ): bool {
178 if ( in_array( $ip, $this->get_default_ip_whitelisted(), true ) ) {
179 return true;
180 }
181
182 // If the IP is the server public IP.
183 $server_public_ip = wd_di()->get( Firewall::class )->get_whitelist_server_public_ip();
184 if ( '' !== $server_public_ip && $server_public_ip === $ip ) {
185 return true;
186 }
187
188 // If the IP is the MainWP Dashboard public IP.
189 $mainwp_dashboard_public_ip = wd_di()->get( Main_Wp::class )->get_whitelist_dashboard_public_ip();
190 if ( array() !== $mainwp_dashboard_public_ip && in_array( $ip, $mainwp_dashboard_public_ip, true ) ) {
191 return true;
192 }
193
194 $blacklist_settings = new Model_Blacklist_Lockout();
195
196 return $this->is_ip_in_format( $ip, $blacklist_settings->get_list( 'allowlist' ) );
197 }
198
199 /**
200 * Checks if an IP is blacklisted.
201 *
202 * @param string $ip The IP address to check.
203 *
204 * @return bool True if the IP is blacklisted, false otherwise.
205 */
206 public function is_blacklist( $ip ) {
207 $blacklist_settings = new Model_Blacklist_Lockout();
208
209 return $this->is_ip_in_format( $ip, $blacklist_settings->get_list( 'blocklist' ) );
210 }
211
212 /**
213 * Checks if a country is blacklisted based on the IP address.
214 *
215 * @param string $ip The IP address to check.
216 *
217 * @return bool True if the country is blacklisted, false otherwise.
218 * @throws InvalidDatabaseException Thrown for unexpected data is found in DB.
219 */
220 public function is_country_blacklist( $ip ): bool {
221 // Check Firewall > IP Banning > Locations section is activated or not.
222 $country = $this->get_current_country( $ip );
223 if ( false === $country ) {
224 return false;
225 }
226 $blacklist_settings = new Model_Blacklist_Lockout();
227 $blacklisted = $blacklist_settings->get_country_blacklist();
228 if ( array() === $blacklisted ) {
229 return false;
230 }
231 if ( in_array( 'all', $blacklisted, true ) ) {
232 return true;
233 }
234
235 if ( isset( $country['iso'] ) && '' !== $country['iso'] ) {
236 $country_iso = strtoupper( $country['iso'] );
237
238 // Check if the specific country is in the blacklist.
239 if ( in_array( $country_iso, $blacklisted, true ) ) {
240 return true;
241 }
242
243 // Check if any continent containing this country is in the blacklist.
244 foreach ( $blacklisted as $blocked_code ) {
245 if ( $this->is_country_in_continent( $country_iso, $blocked_code ) ) {
246 return true;
247 }
248 }
249 }
250
251 return false;
252 }
253
254 /**
255 * Verifies the format of an imported file for IP lockout settings.
256 *
257 * @param string $file Path to the file to verify.
258 *
259 * @return array|bool Array of data if the file is valid, false otherwise.
260 */
261 public function verify_import_file( $file ) {
262 global $wp_filesystem;
263 // Initialize the WP filesystem, no more using 'file-put-contents' function.
264 if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
265 require_once ABSPATH . '/wp-admin/includes/file.php';
266 WP_Filesystem();
267 }
268 $lines = $wp_filesystem->get_contents_array( $file );
269 $data = array();
270 foreach ( $lines as $line ) {
271 $line = str_getcsv( $line, ',', '"', '\\' );
272 if ( count( $line ) !== 2 ) {
273 return false;
274 }
275 if ( ! in_array( $line[1], array( 'allowlist', 'blocklist' ), true ) ) {
276 return false;
277 }
278 if ( false === $this->validate_ip( $line[0] ) ) {
279 continue;
280 }
281 $data[] = $line;
282 }
283 return $data;
284 }
285
286 /**
287 * Adds a default whitelisted country to the model.
288 *
289 * @param Model_Blacklist_Lockout $model The model to update.
290 * @param string $country_iso The ISO code of the country to whitelist.
291 *
292 * @return Model_Blacklist_Lockout The updated model.
293 * @since 2.8.0
294 */
295 public function add_default_whitelisted_country( Model_Blacklist_Lockout $model, $country_iso ) {
296 if ( array() === $model->country_whitelist ) {
297 $model->country_whitelist[] = $country_iso;
298 } elseif ( ! in_array( $country_iso, $model->country_whitelist, true ) ) {
299 $model->country_whitelist[] = $country_iso;
300 }
301
302 return $model;
303 }
304
305 /**
306 * Check downloaded GeoDB.
307 *
308 * @return bool
309 * @throws InvalidDatabaseException Thrown for unexpected data is found in DB.
310 */
311 public function is_geodb_downloaded(): bool {
312 $model = new Model_Blacklist_Lockout();
313 $license_key = $model->maxmind_license_key;
314 // Using Geo DB without a license key is not advisable.
315 if ( '' === $license_key && is_file( $model->geodb_path ) ) {
316 $service_geo = wd_di()->get( MaxMind_Geolocation::class );
317 $service_geo->delete_database();
318 $model->geodb_path = '';
319 $model->save();
320
321 return false;
322 }
323
324 // Likely the case after the config import with the existed MaxMind license key.
325 if (
326 '' !== $license_key
327 && ( is_null( $model->geodb_path ) || ! is_file( $model->geodb_path ) )
328 ) {
329 $service_geo = wd_di()->get( MaxMind_Geolocation::class );
330 $tmp = $service_geo->get_downloaded_url( $license_key );
331 if ( ! is_wp_error( $tmp ) ) {
332 $phar = new PharData( $tmp );
333 $path = $service_geo->get_db_base_path();
334 if ( ! is_dir( $path ) ) {
335 wp_mkdir_p( $path );
336 }
337 $phar->extractTo( $path, null, true );
338 $model->geodb_path = $path . DIRECTORY_SEPARATOR . $phar->current()->getFileName() . DIRECTORY_SEPARATOR . $service_geo->get_db_full_name();
339 // Save because we'll check for a saved path.
340 $model->save();
341
342 if ( file_exists( $tmp ) ) {
343 wp_delete_file( $tmp );
344 }
345
346 if ( array() === $model->country_whitelist ) {
347 $is_country = false;
348 foreach ( $this->get_user_ip() as $ip ) {
349 $country = $this->get_current_country( $ip );
350
351 if ( isset( $country['iso'] ) && '' !== $country['iso'] ) {
352 $model = $this->add_default_whitelisted_country( $model, $country['iso'] );
353 $is_country = true;
354 }
355 }
356
357 if ( false === $is_country ) {
358 return false;
359 }
360 }
361 $model->save();
362 }
363 }
364
365 // Check again.
366 if ( is_null( $model->geodb_path ) || ! is_file( $model->geodb_path ) ) {
367 return false;
368 }
369
370 // Check if the file exists on the site. The file can exist on the same server but for different sites.
371 // For example, after config importing.
372 $path_parts = pathinfo( $model->geodb_path );
373 if ( preg_match( '/(\/wp-content\/.+)/', $path_parts['dirname'], $matches ) ) {
374 $rel_path = $matches[1];
375 $rel_path = ltrim( $rel_path, '/' );
376 $abs_path = ABSPATH . $rel_path;
377 if ( ! is_dir( $abs_path ) ) {
378 wp_mkdir_p( $abs_path );
379 }
380
381 $rel_path = $abs_path . DIRECTORY_SEPARATOR . $path_parts['basename'];
382 if ( file_exists( $rel_path ) ) {
383 return true;
384 } elseif ( ! is_null( $model->geodb_path ) && '' !== $model->geodb_path && file_exists( $model->geodb_path ) ) {
385 // The case if ABSPATH was changed e.g. in wp-config.php.
386 return true;
387 }
388
389 if ( $this->validate_geodb_file( $model->geodb_path ) ) {
390 global $wp_filesystem;
391 if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
392 require_once ABSPATH . '/wp-admin/includes/file.php';
393 WP_Filesystem();
394 }
395
396 if ( $wp_filesystem->move( $model->geodb_path, $rel_path ) ) {
397 $wp_filesystem->chmod( $rel_path, 0644 );
398 $model->geodb_path = $rel_path;
399 $model->save();
400 }
401 } else {
402 return false;
403 }
404 }
405
406 return true;
407 }
408
409 /**
410 * Validates GeoDB file for security before processing.
411 *
412 * @param string $file_path Path to the file to validate.
413 * @return bool True if file is valid, false otherwise.
414 */
415 private function validate_geodb_file( $file_path ): bool {
416 if ( ! file_exists( $file_path ) ) {
417 return false;
418 }
419
420 $allowed_extensions = array( 'mmdb' );
421 $file_extension = strtolower( pathinfo( $file_path, PATHINFO_EXTENSION ) );
422
423 if ( ! in_array( $file_extension, $allowed_extensions, true ) ) {
424 return false;
425 }
426
427 $file_size = filesize( $file_path );
428 $max_size = 100 * 1024 * 1024; // 100MB.
429
430 if ( $max_size < $file_size || 0 === $file_size ) {
431 return false;
432 }
433
434 global $wp_filesystem;
435 if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
436 require_once ABSPATH . 'wp-admin/includes/file.php';
437 WP_Filesystem();
438 }
439
440 // Read only first 4 bytes for header validation.
441 $header = $wp_filesystem->get_contents( $file_path );
442 if ( false === $header || strlen( $header ) < 4 ) {
443 return false;
444 }
445 $header = substr( $header, 0, 4 );
446 // MaxMind DB format magic bytes: 0xABCDEF00 .
447 if ( "\xab\xcd\xef\x00" !== $header ) {
448 return false;
449 }
450
451 return true;
452 }
453
454 /**
455 * Retrieves the top countries from which IPs have been blocked.
456 *
457 * @param int $limit The maximum number of countries to return.
458 * @param int $max_age_days The maximum age of log entries to consider.
459 *
460 * @return array List of countries and their blocked IP count.
461 */
462 public function get_top_countries_blocked( $limit = 10, $max_age_days = 7 ) {
463 $result = Array_Cache::get( 'countries', 'ip_lockout', array() );
464 if ( array() === $result ) {
465 global $wpdb;
466
467 $result = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
468 $wpdb->prepare(
469 "SELECT country_iso_code, COUNT(ip) AS ip_count FROM {$wpdb->base_prefix}defender_lockout_log WHERE (type = %s OR type = %s OR type = %s) AND date >= %d AND country_iso_code IS NOT NULL GROUP BY country_iso_code ORDER BY ip_count DESC LIMIT %d",
470 Lockout_Log::LOCKOUT_404,
471 Lockout_Log::AUTH_LOCK,
472 Lockout_Log::LOCKOUT_UA,
473 strtotime( '-' . $max_age_days . ' days', time() ),
474 $limit
475 ),
476 ARRAY_A
477 );
478 // Get data from cache.
479 Array_Cache::set( 'countries', $result, 'ip_lockout' );
480 }
481
482 return array() !== $result ? $result : array();
483 }
484
485 /**
486 * Is IP on BLC Whitelist?
487 *
488 * @return bool
489 * @since 4.2.0
490 */
491 public function is_blc_ip_whitelisted(): bool {
492 $ips = $this->get_user_ip();
493 $blc_ips = array(
494 '165.227.127.103',
495 '64.176.196.23',
496 '144.202.86.106',
497 );
498 $diff = array_diff( $ips, $blc_ips );
499
500 return array() === $diff;
501 }
502
503 /**
504 * Checks if a list of IPs are all whitelisted.
505 *
506 * @param array $ips List of IPs to check.
507 *
508 * @return bool True if all IPs are whitelisted, false otherwise.
509 * @since 4.4.2
510 */
511 public function are_ips_whitelisted( array $ips ): bool {
512 foreach ( $ips as $ip ) {
513 if ( ! $this->is_ip_whitelisted( $ip ) ) {
514 return false;
515 }
516 }
517
518 return true;
519 }
520 }
521