PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.3.1
Defender Security – Malware Scanner, Login Security & Firewall v5.3.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.3.1, at src/component/class-blacklist-lockout.php

403 lines 11.5 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\Model\Lockout_Log;
16 use MaxMind\Db\Reader\InvalidDatabaseException;
17 use WP_Defender\Integrations\MaxMind_Geolocation;
18 use WP_Defender\Model\Setting\Blacklist_Lockout as Model_Blacklist_Lockout;
19 use WP_Defender\Component\Firewall;
20 use WP_Defender\Integrations\Main_Wp;
21
22 /**
23 * Handles operations related to IP and country-based blacklisting and whitelisting.
24 */
25 class Blacklist_Lockout extends Component {
26
27 use Country;
28 use IP;
29
30 // Define the transient key.
31 const IP_LIST_KEY = 'wpmu_dev_ip_list';
32
33 /**
34 * Checks if a given IP is whitelisted based on the country.
35 *
36 * @param string $ip The IP address to check.
37 *
38 * @return bool True if the IP is whitelisted based on the country, false otherwise.
39 * @throws InvalidDatabaseException Thrown for unexpected data is found in DB.
40 */
41 public function is_country_whitelist( $ip ): bool {
42 // Check Firewall > IP Banning > Locations section is activated or not.
43 $country = $this->get_current_country( $ip );
44 if ( false === $country ) {
45 return false;
46 }
47 $model = new Model_Blacklist_Lockout();
48 $whitelist = $model->get_country_whitelist();
49 if ( empty( $whitelist ) ) {
50 return false;
51 }
52 if ( ! empty( $country['iso'] ) && in_array( strtoupper( $country['iso'] ), $whitelist, true ) ) {
53 return true;
54 }
55
56 return false;
57 }
58
59 /**
60 * Return the default ips need to whitelisted, e.g. HUB ips.
61 *
62 * @return array
63 */
64 private function get_default_ip_whitelisted(): array {
65 $server_addr = defender_get_data_from_request( 'SERVER_ADDR', 's' );
66 $remote_addr = defender_get_data_from_request( 'REMOTE_ADDR', 's' );
67 $ips = array(
68 ...$this->fetch_latest_ips(),
69 '127.0.0.1',
70 isset( $server_addr ) ? $server_addr : $remote_addr,
71 );
72
73 return (array) apply_filters( 'ip_lockout_default_whitelist_ip', $ips );
74 }
75
76 /**
77 * Fetch the latest IP list from the API.
78 *
79 * @return array List of IPs or an empty array on failure.
80 */
81 public static function fetch_latest_ips(): array {
82 // Attempt to retrieve IPs from the cache.
83 $cached_ips = get_site_transient( self::IP_LIST_KEY );
84 if ( $cached_ips && is_array( $cached_ips ) ) {
85 return $cached_ips;
86 }
87
88 // Fetch the IP list from the remote API.
89 $response = wp_remote_get(
90 'https://gist.githubusercontent.com/wpmu-docs/568114153e93eaf28f908a724b313b6f/raw',
91 array(
92 'timeout' => 10,
93 )
94 );
95
96 // Check for errors in the API response.
97 if ( is_wp_error( $response ) ) {
98 return array();
99 }
100
101 // Retrieve the response body.
102 $body = wp_remote_retrieve_body( $response );
103 if ( empty( $body ) ) {
104 return array();
105 }
106
107 // Process the response body into an array of IPs.
108 $ip_list = array_filter( array_map( 'sanitize_text_field', explode( "\n", $body ) ) );
109
110 // Cache the IP list for a week if it is not empty.
111 if ( ! empty( $ip_list ) ) {
112 set_site_transient( self::IP_LIST_KEY, $ip_list, WEEK_IN_SECONDS );
113 }
114
115 // Return the fetched IP list.
116 return $ip_list;
117 }
118
119 /**
120 * Checks if an IP is whitelisted.
121 *
122 * @param string $ip The IP address to check.
123 *
124 * @return bool True if the IP is whitelisted, false otherwise.
125 */
126 public function is_ip_whitelisted( $ip ): bool {
127 if ( in_array( $ip, $this->get_default_ip_whitelisted(), true ) ) {
128 return true;
129 }
130
131 // If the IP is the server public IP.
132 $server_public_ip = wd_di()->get( Firewall::class )->get_whitelist_server_public_ip();
133 if ( ! empty( $server_public_ip ) && $server_public_ip === $ip ) {
134 return true;
135 }
136
137 // If the IP is the MainWP Dashboard public IP.
138 $mainwp_dashboard_public_ip = wd_di()->get( Main_Wp::class )->get_whitelist_dashboard_public_ip();
139 if ( ! empty( $mainwp_dashboard_public_ip ) && in_array( $ip, $mainwp_dashboard_public_ip, true ) ) {
140 return true;
141 }
142
143 $blacklist_settings = new Model_Blacklist_Lockout();
144
145 return $this->is_ip_in_format( $ip, $blacklist_settings->get_list( 'allowlist' ) );
146 }
147
148 /**
149 * Checks if an IP is blacklisted.
150 *
151 * @param string $ip The IP address to check.
152 *
153 * @return bool True if the IP is blacklisted, false otherwise.
154 */
155 public function is_blacklist( $ip ) {
156 $blacklist_settings = new Model_Blacklist_Lockout();
157
158 return $this->is_ip_in_format( $ip, $blacklist_settings->get_list( 'blocklist' ) );
159 }
160
161 /**
162 * Checks if a country is blacklisted based on the IP address.
163 *
164 * @param string $ip The IP address to check.
165 *
166 * @return bool True if the country is blacklisted, false otherwise.
167 * @throws InvalidDatabaseException Thrown for unexpected data is found in DB.
168 */
169 public function is_country_blacklist( $ip ): bool {
170 // Check Firewall > IP Banning > Locations section is activated or not.
171 $country = $this->get_current_country( $ip );
172 if ( false === $country ) {
173 return false;
174 }
175 $blacklist_settings = new Model_Blacklist_Lockout();
176 $blacklisted = $blacklist_settings->get_country_blacklist();
177 if ( empty( $blacklisted ) ) {
178 return false;
179 }
180 if ( in_array( 'all', $blacklisted, true ) ) {
181 return true;
182 }
183 if ( ! empty( $country['iso'] ) && in_array( strtoupper( $country['iso'] ), $blacklisted, true ) ) {
184 return true;
185 }
186
187 return false;
188 }
189
190 /**
191 * Verifies the format of an imported file for IP lockout settings.
192 *
193 * @param string $file Path to the file to verify.
194 *
195 * @return array|bool Array of data if the file is valid, false otherwise.
196 */
197 public function verify_import_file( $file ) {
198 global $wp_filesystem;
199 // Initialize the WP filesystem, no more using 'file-put-contents' function.
200 if ( empty( $wp_filesystem ) ) {
201 require_once ABSPATH . '/wp-admin/includes/file.php';
202 WP_Filesystem();
203 }
204 $lines = $wp_filesystem->get_contents_array( $file );
205 $data = array();
206 foreach ( $lines as $line ) {
207 $line = str_getcsv( $line, ',', '"', '\\' );
208 if ( count( $line ) !== 2 ) {
209 return false;
210 }
211 if ( ! in_array( $line[1], array( 'allowlist', 'blocklist' ), true ) ) {
212 return false;
213 }
214 if ( false === $this->validate_ip( $line[0] ) ) {
215 continue;
216 }
217 $data[] = $line;
218 }
219 return $data;
220 }
221
222 /**
223 * Adds a default whitelisted country to the model.
224 *
225 * @param Model_Blacklist_Lockout $model The model to update.
226 * @param string $country_iso The ISO code of the country to whitelist.
227 *
228 * @return Model_Blacklist_Lockout The updated model.
229 * @since 2.8.0
230 */
231 public function add_default_whitelisted_country( Model_Blacklist_Lockout $model, $country_iso ) {
232 if ( empty( $model->country_whitelist ) ) {
233 $model->country_whitelist[] = $country_iso;
234 } elseif ( ! in_array( $country_iso, $model->country_whitelist, true ) ) {
235 $model->country_whitelist[] = $country_iso;
236 }
237
238 return $model;
239 }
240
241 /**
242 * Check downloaded GeoDB.
243 *
244 * @return bool
245 * @throws InvalidDatabaseException Thrown for unexpected data is found in DB.
246 */
247 public function is_geodb_downloaded(): bool {
248 $model = new Model_Blacklist_Lockout();
249 $license_key = $model->maxmind_license_key;
250 // Using Geo DB without a license key is not advisable.
251 if ( empty( $license_key ) && is_file( $model->geodb_path ) ) {
252 $service_geo = wd_di()->get( MaxMind_Geolocation::class );
253 $service_geo->delete_database();
254 $model->geodb_path = '';
255 $model->save();
256
257 return false;
258 }
259
260 // Likely the case after the config import with the existed MaxMind license key.
261 if (
262 ! empty( $license_key )
263 && ( is_null( $model->geodb_path ) || ! is_file( $model->geodb_path ) )
264 ) {
265 $service_geo = wd_di()->get( MaxMind_Geolocation::class );
266 $tmp = $service_geo->get_downloaded_url( $license_key );
267 if ( ! is_wp_error( $tmp ) ) {
268 $phar = new PharData( $tmp );
269 $path = $service_geo->get_db_base_path();
270 if ( ! is_dir( $path ) ) {
271 wp_mkdir_p( $path );
272 }
273 $phar->extractTo( $path, null, true );
274 $model->geodb_path = $path . DIRECTORY_SEPARATOR . $phar->current()->getFileName() . DIRECTORY_SEPARATOR . $service_geo->get_db_full_name();
275 // Save because we'll check for a saved path.
276 $model->save();
277
278 if ( file_exists( $tmp ) ) {
279 wp_delete_file( $tmp );
280 }
281
282 if ( empty( $model->country_whitelist ) ) {
283 $is_country = false;
284 foreach ( $this->get_user_ip() as $ip ) {
285 $country = $this->get_current_country( $ip );
286
287 if ( ! empty( $country['iso'] ) ) {
288 $model = $this->add_default_whitelisted_country( $model, $country['iso'] );
289 $is_country = true;
290 }
291 }
292
293 if ( false === $is_country ) {
294 return false;
295 }
296 }
297 $model->save();
298 }
299 }
300
301 // Check again.
302 if ( is_null( $model->geodb_path ) || ! is_file( $model->geodb_path ) ) {
303 return false;
304 }
305
306 // Check if the file exists on the site. The file can exist on the same server but for different sites.
307 // For example, after config importing.
308 $path_parts = pathinfo( $model->geodb_path );
309 if ( preg_match( '/(\/wp-content\/.+)/', $path_parts['dirname'], $matches ) ) {
310 $rel_path = $matches[1];
311 $rel_path = ltrim( $rel_path, '/' );
312 $abs_path = ABSPATH . $rel_path;
313 if ( ! is_dir( $abs_path ) ) {
314 wp_mkdir_p( $abs_path );
315 }
316
317 $rel_path = $abs_path . DIRECTORY_SEPARATOR . $path_parts['basename'];
318 if ( file_exists( $rel_path ) ) {
319 return true;
320 } elseif ( ! empty( $model->geodb_path ) && file_exists( $model->geodb_path ) ) {
321 // The case if ABSPATH was changed e.g. in wp-config.php.
322 return true;
323 }
324
325 if ( move_uploaded_file( $model->geodb_path, $rel_path ) ) {
326 $model->geodb_path = $rel_path;
327 $model->save();
328 } else {
329 return false;
330 }
331 }
332
333 return true;
334 }
335
336 /**
337 * Retrieves the top countries from which IPs have been blocked.
338 *
339 * @param int $limit The maximum number of countries to return.
340 * @param int $max_age_days The maximum age of log entries to consider.
341 *
342 * @return array List of countries and their blocked IP count.
343 */
344 public function get_top_countries_blocked( $limit = 10, $max_age_days = 7 ) {
345 $result = Array_Cache::get( 'countries', 'ip_lockout', array() );
346 if ( empty( $result ) ) {
347 global $wpdb;
348
349 $result = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
350 $wpdb->prepare(
351 "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",
352 Lockout_Log::LOCKOUT_404,
353 Lockout_Log::AUTH_LOCK,
354 Lockout_Log::LOCKOUT_UA,
355 strtotime( '-' . $max_age_days . ' days', time() ),
356 $limit
357 ),
358 ARRAY_A
359 );
360 // Get data from cache.
361 Array_Cache::set( 'countries', $result, 'ip_lockout' );
362 }
363
364 return ! empty( $result ) ? $result : array();
365 }
366
367 /**
368 * Is IP on BLC Whitelist?
369 *
370 * @return bool
371 * @since 4.2.0
372 */
373 public function is_blc_ip_whitelisted(): bool {
374 $ips = $this->get_user_ip();
375 $blc_ips = array(
376 '165.227.127.103',
377 '64.176.196.23',
378 '144.202.86.106',
379 );
380 $diff = array_diff( $ips, $blc_ips );
381
382 return empty( $diff );
383 }
384
385 /**
386 * Checks if a list of IPs are all whitelisted.
387 *
388 * @param array $ips List of IPs to check.
389 *
390 * @return bool True if all IPs are whitelisted, false otherwise.
391 * @since 4.4.2
392 */
393 public function are_ips_whitelisted( array $ips ): bool {
394 foreach ( $ips as $ip ) {
395 if ( ! $this->is_ip_whitelisted( $ip ) ) {
396 return false;
397 }
398 }
399
400 return true;
401 }
402 }
403