PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.2
Defender Security – Malware Scanner, Login Security & Firewall v6.2.2
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 6.2.2, at src/component/class-blacklist-lockout.php

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