PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.9.0
Defender Security – Malware Scanner, Login Security & Firewall v5.9.0
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-firewall.php

class-firewall.php in Defender Security – Malware Scanner, Login Security & Firewall 5.9.0, at src/component/class-firewall.php

621 lines 17.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles operations related to IP and country-based blacklisting and
4 * whitelisting, as well as managing firewall settings and logs.
5 *
6 * @package WP_Defender\Component
7 */
8
9 namespace WP_Defender\Component;
10
11 use Exception;
12 use WPMUDEV_Dashboard;
13 use WP_Defender\Component;
14 use WP_Defender\Model\Lockout_Ip;
15 use WP_Defender\Behavior\WPMUDEV;
16 use WP_Defender\Model\Lockout_Log;
17 use WP_Defender\Model\Onboard;
18 use MaxMind\Db\Reader\InvalidDatabaseException;
19 use WP_Defender\Model\Setting\Firewall as Model_Firewall;
20 use WP_Defender\Component\Trusted_Proxy_Preset\Cloudflare_Proxy;
21 use WP_Defender\Component\Trusted_Proxy_Preset\Trusted_Proxy_Preset;
22 use WP_Defender\Controller\Firewall as Firewall_Controller;
23 use WP_Defender\Component\Smart_Ip_Detection;
24
25 /**
26 * Handles operations related to IP and country-based blacklisting and whitelisting,
27 * managing firewall settings, logs, and automatic detection of proxy configurations.
28 */
29 class Firewall extends Component {
30
31 /**
32 * The notice slug if there is switching IP Detection option to Cloudflare (CF).
33 */
34 public const IP_DETECTION_CF_SHOW_SLUG = 'wd_show_ip_detection_cf_notice';
35
36 /**
37 * The notice slug if CF IP Detection notice is rejected.
38 */
39 public const IP_DETECTION_CF_DISMISS_SLUG = 'wd_dismiss_ip_detection_cf_notice';
40
41 /**
42 * The notice slug if there is switching IP Detection option to X-Forwarded-For (XFF).
43 */
44 public const IP_DETECTION_XFF_SHOW_SLUG = 'wd_show_ip_detection_xff_notice';
45
46 /**
47 * The notice slug if CF IP Detection notice is rejected.
48 */
49 public const IP_DETECTION_XFF_DISMISS_SLUG = 'wd_dismiss_ip_detection_xff_notice';
50
51 /**
52 * The option name for the whitelist server public IP.
53 */
54 public const WHITELIST_SERVER_PUBLIC_IP_OPTION = 'wpdef_firewall_whitelist_server_public_ip';
55
56 /**
57 * Check if the first commencing request is proper staff remote access.
58 *
59 * @param array $access The access details including key.
60 *
61 * @return bool
62 */
63 private function is_commencing_staff_access( $access ): bool {
64 $action = defender_get_data_from_request( 'action', 'g' );
65 $wdpunkey = defender_get_data_from_request( 'wdpunkey', 'p' );
66
67 return wp_doing_ajax() && 'wdpunauth' === $action && isset( $access['key'] ) && hash_equals( $wdpunkey, $access['key'] );
68 }
69
70 /**
71 * Check is the access from authenticated staff.
72 *
73 * @return bool
74 */
75 private function is_authenticated_staff_access(): bool {
76 return '1' === defender_get_data_from_request( 'wpmudev_is_staff', 'c' );
77 }
78
79 /**
80 * Check if the access is from our staff access.
81 *
82 * @return bool
83 */
84 private function is_a_staff_access(): bool {
85 if ( defined( 'WPMUDEV_DISABLE_REMOTE_ACCESS' ) && true === constant( 'WPMUDEV_DISABLE_REMOTE_ACCESS' ) ) {
86 return false;
87 }
88
89 $wpmu_dev = new WPMUDEV();
90 $is_remote_access = $wpmu_dev->get_apikey() &&
91 true === WPMUDEV_Dashboard::$api->remote_access_details( 'enabled' );
92
93 if ( $is_remote_access ) {
94 $access = $wpmu_dev->get_remote_access();
95 if ( $this->is_authenticated_staff_access() || $this->is_commencing_staff_access( $access ) ) {
96 $this->log( $access, Firewall_Controller::FIREWALL_LOG );
97
98 return true;
99 }
100 }
101
102 return false;
103 }
104
105 /**
106 * Cron for delete old log.
107 *
108 * @throws Exception On failure.
109 */
110 public function firewall_clean_up_logs() {
111 $settings = new Model_Firewall();
112 $storage_days = apply_filters( 'ip_lockout_logs_store_backward', $settings->storage_days );
113 if ( ! is_numeric( $storage_days ) ) {
114 return;
115 }
116 $time_string = '-' . $storage_days . ' days';
117 $timestamp = $this->local_to_utc( $time_string );
118 // Since v5.7.0.
119 $delete_count = apply_filters( 'wpdef_firewall_limit_deleted_logs', 50 );
120 $delete_count = is_int( $delete_count ) ? $delete_count : (int) $delete_count;
121 Lockout_Log::remove_logs( $timestamp, $delete_count );
122 }
123
124 /**
125 * Cron for clean up temporary IP block list.
126 */
127 public function firewall_clean_up_temporary_ip_blocklist() {
128 $models = Lockout_Ip::get_bulk( Lockout_Ip::STATUS_BLOCKED );
129 foreach ( $models as $model ) {
130 $model->status = Lockout_Ip::STATUS_NORMAL;
131 $model->save();
132 }
133 }
134
135 /**
136 * Update temporary IP block list of Firewall, clear cron job.
137 * The interval settings value is updated once.
138 *
139 * @param string $new_interval The new interval to set.
140 */
141 public function update_cron_schedule_interval( $new_interval ) {
142 $settings = new Model_Firewall();
143 // If a new interval is different from the saved value, we need to clear the cron job.
144 if ( $new_interval !== $settings->ip_blocklist_cleanup_interval ) {
145 update_site_option( 'wpdef_clear_schedule_firewall_cleanup_temp_blocklist_ips', true );
146 }
147 }
148
149 /**
150 * Skip priority lockout checks for a given IP.
151 *
152 * @param string $ip The IP address to check.
153 *
154 * @return bool True if the IP should be skipped from lockout checks, false otherwise.
155 * @throws InvalidDatabaseException Thrown for unexpected data is found in DB.
156 */
157 public function skip_priority_lockout_checks( string $ip ): bool {
158 /**
159 * Retrieve Global_IP instance.
160 *
161 * @var IP\Global_IP $global_ip
162 */
163 $global_ip = wd_di()->get( IP\Global_IP::class );
164
165 if (
166 $global_ip->is_global_ip_enabled() &&
167 $global_ip->is_ip_allowed( $ip )
168 ) {
169 return true;
170 }
171
172 /**
173 * Retrieve Blacklist_Lockout instance.
174 *
175 * @var Blacklist_Lockout $service
176 */
177 $service = wd_di()->get( Blacklist_Lockout::class );
178
179 $model = Lockout_Ip::get( $ip );
180 $is_lockout_ip = is_object( $model ) && $model->is_locked();
181
182 $is_country_whitelisted = ! $service->is_blacklist( $ip ) &&
183 $service->is_country_whitelist( $ip ) && ! $is_lockout_ip;
184
185 // If this IP is whitelisted, so we don't need to blacklist this.
186 if ( $service->is_ip_whitelisted( $ip ) || $is_country_whitelisted ) {
187 return true;
188 }
189 // Green light if access staff is enabled.
190 if ( $this->is_a_staff_access() ) {
191 return true;
192 }
193
194 return false;
195 }
196
197 /**
198 * Check if an IP is blacklisted.
199 *
200 * @param string $ip The IP address to check.
201 *
202 * @return array An array containing the reason and result of the blacklist check.
203 * @throws InvalidDatabaseException Thrown for unexpected data is found in DB.
204 */
205 public function is_blocklisted_ip( string $ip ): array {
206 $array = array(
207 'reason' => '',
208 'result' => false,
209 );
210 /**
211 * Retrieve Blacklist_Lockout instance.
212 *
213 * @var Blacklist_Lockout $service
214 */
215 $service = wd_di()->get( Blacklist_Lockout::class );
216
217 if ( $service->is_blacklist( $ip ) ) {
218 return array(
219 'reason' => 'local_ip',
220 'result' => true,
221 );
222 }
223
224 if ( $service->is_country_blacklist( $ip ) ) {
225 return array(
226 'reason' => 'country',
227 'result' => true,
228 );
229 }
230
231 /**
232 * Retrieve Global_IP instance.
233 *
234 * @var IP\Global_IP $global_ip
235 */
236 $global_ip = wd_di()->get( IP\Global_IP::class );
237
238 if (
239 $global_ip->is_active() &&
240 $global_ip->is_ip_blocked( $ip )
241 ) {
242 return array(
243 'reason' => IP\Global_IP::REASON_SLUG,
244 'result' => true,
245 );
246 }
247
248 /**
249 * Retrieve Antibot_Global_Firewall instance.
250 *
251 * @var IP\Antibot_Global_Firewall $antibot
252 */
253 $antibot = wd_di()->get( IP\Antibot_Global_Firewall::class );
254
255 if (
256 $antibot->is_active() &&
257 $antibot->is_ip_blocked( $ip ) &&
258 'plugin' === $antibot->get_managed_by()
259 ) {
260 return array(
261 'reason' => IP\Antibot_Global_Firewall::REASON_SLUG,
262 'result' => true,
263 );
264 }
265
266 return $array;
267 }
268
269 /**
270 * Get the limit of Lockout records.
271 *
272 * @return int
273 * @since 3.7.0 Get the limit of Lockout records.
274 */
275 public function get_lockout_record_limit() {
276 $limit = apply_filters( 'wd_lockout_record_limit', 10000 );
277 return is_int( $limit ) ? $limit : (int) $limit;
278 }
279
280 /**
281 * Cron for deleting unwanted lockout records.
282 *
283 * @return void
284 * @since 3.8.0
285 */
286 public function firewall_clean_up_lockout(): void {
287 global $wpdb;
288
289 $current_timestamp = time();
290 $limit = $this->get_lockout_record_limit();
291 $table = $wpdb->base_prefix . ( new Lockout_Ip() )->get_table();
292
293 do {
294 $affected_rows = $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
295 $wpdb->prepare(
296 "DELETE FROM {$table} WHERE (release_time = 0 OR release_time < %d) AND meta IN (%s, %s, %s, %s, %s) ORDER BY id LIMIT %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
297 $current_timestamp,
298 '[]',
299 '{"nf":[]}',
300 '{"login":[]}',
301 '{"nf":[],"login":[]}',
302 '{"login":[],"nf":[]}',
303 $limit
304 )
305 );
306
307 } while ( $affected_rows === $limit );
308 }
309
310 /**
311 * Gather IP(s) from headers.
312 *
313 * @since 4.4.2
314 * @deprecated 5.1.0 This method and the 'wpdef_firewall_gathered_ips' filter are no longer in use.
315 * @return array
316 */
317 public function gather_ips(): array {
318 $ip_headers = array(
319 'HTTP_CLIENT_IP',
320 'HTTP_X_REAL_IP',
321 'HTTP_X_FORWARDED_FOR',
322 'HTTP_X_FORWARDED',
323 'HTTP_X_CLUSTER_CLIENT_IP',
324 'HTTP_FORWARDED_FOR',
325 'HTTP_FORWARDED',
326 'HTTP_CF_CONNECTING_IP',
327 'REMOTE_ADDR',
328 );
329
330 $gathered_ips = array();
331 $server = defender_get_data_from_request( null, 's' );
332 foreach ( $ip_headers as $header ) {
333 if ( isset( $server[ $header ] ) && '' !== $server[ $header ] ) {
334 // Handle multiple IP addresses.
335 $ips = array_map( 'trim', explode( ',', $server[ $header ] ) );
336
337 foreach ( $ips as $ip ) {
338 if ( $this->validate_ip( $ip ) ) {
339 $gathered_ips[] = $ip;
340 }
341 }
342 }
343 }
344
345 /**
346 * Filter the gathered IPs before checking the lockout records.
347 *
348 * @param array $gathered_ips IPs gathered from request headers.
349 *
350 * @since 4.5.1
351 * @deprecated 5.1.0 No longer used and will be removed in a future version.
352 */
353 $gathered_ips = apply_filters_deprecated(
354 'wpdef_firewall_gathered_ips',
355 array_unique( $gathered_ips ),
356 '5.1.0',
357 '',
358 'This filter will be removed in a future version. If you are using it, update your implementation.'
359 );
360 $gathered_ips = ! is_array( $gathered_ips ) ? (array) $gathered_ips : $gathered_ips;
361
362 return $this->filter_user_ips( $gathered_ips );
363 }
364
365 /**
366 * Check if the current request is recognized as coming from Cloudflare.
367 *
368 * @return bool
369 */
370 public function is_cloudflare_request(): bool {
371 $is_cloudflare = true;
372
373 $server = defender_get_data_from_request( null, 's' );
374 if ( ! (
375 isset( $server['HTTP_CF_CONNECTING_IP'] ) ||
376 isset( $server['HTTP_CF_IPCOUNTRY'] ) ||
377 isset( $server['HTTP_CF_RAY'] ) ||
378 isset( $server['HTTP_CF_VISITOR'] )
379 ) ) {
380 $is_cloudflare = false;
381 }
382
383 return $is_cloudflare;
384 }
385
386 /**
387 * Auto-detect proxy server and switch to appropriate IP Detection option.
388 *
389 * @return void
390 */
391 public function auto_switch_ip_detection_option(): void {
392 $model = wd_di()->get( Model_Firewall::class );
393
394 if ( $this->is_cloudflare_request() ) {
395 if (
396 'HTTP_CF_CONNECTING_IP' !== $model->http_ip_header &&
397 ! self::is_switched_ip_detection_notice( self::IP_DETECTION_CF_SHOW_SLUG )
398 ) {
399 $model->http_ip_header = 'HTTP_CF_CONNECTING_IP';
400 update_site_option( self::IP_DETECTION_CF_SHOW_SLUG, true );
401 }
402
403 $model->trusted_proxy_preset = Cloudflare_Proxy::PROXY_SLUG;
404 $model->save();
405
406 // Fetch trusted proxy ips.
407 $this->update_trusted_proxy_preset_ips();
408 }
409 }
410
411 /**
412 * Update trusted proxy preset IPs.
413 *
414 * @return void
415 */
416 public function update_trusted_proxy_preset_ips(): void {
417 $model = wd_di()->get( Model_Firewall::class );
418 if ( '' !== $model->trusted_proxy_preset ) {
419 /**
420 * Retrieve Trusted_Proxy_Preset instance.
421 *
422 * @var Trusted_Proxy_Preset $trusted_proxy_preset
423 */
424 $trusted_proxy_preset = wd_di()->get( Trusted_Proxy_Preset::class );
425 $trusted_proxy_preset->set_proxy_preset( $model->trusted_proxy_preset );
426 $trusted_proxy_preset->update_ips();
427 }
428 }
429
430 /**
431 * Show a notice if wrong IP Detection option is configured for the site.
432 *
433 * @return void
434 */
435 public function maybe_show_misconfigured_ip_detection_option_notice(): void {
436 $model = wd_di()->get( Model_Firewall::class );
437 $xff = defender_get_data_from_request( 'HTTP_X_FORWARDED_FOR', 's' );
438
439 if (
440 'HTTP_X_FORWARDED_FOR' !== $model->http_ip_header &&
441 ( is_string( $xff ) && 0 < strlen( $xff ) ) &&
442 ! $this->is_cloudflare_request() &&
443 ! self::is_xff_notice_ready()
444 ) {
445 update_site_option( self::IP_DETECTION_XFF_SHOW_SLUG, true );
446 }
447 }
448
449 /**
450 * Check if a switched IP detection notice has been shown.
451 *
452 * @param string $key The notice key to check.
453 *
454 * @return bool True if the notice has been shown, false otherwise.
455 */
456 public static function is_switched_ip_detection_notice( string $key ): bool {
457 return (bool) get_site_option( $key );
458 }
459
460 /**
461 * Check if the XFF notice is ready to be shown.
462 *
463 * @return bool
464 */
465 public static function is_xff_notice_ready(): bool {
466 return ! Onboard::maybe_show_onboarding() &&
467 self::is_switched_ip_detection_notice( self::IP_DETECTION_XFF_SHOW_SLUG )
468 && ! self::is_switched_ip_detection_notice( self::IP_DETECTION_XFF_DISMISS_SLUG )
469 && ! wd_di()->get( Smart_Ip_Detection::class )->is_smart_ip_detection_enabled()
470 && 'HTTP_X_FORWARDED_FOR' !== wd_di()->get( Model_Firewall::class )->http_ip_header;
471 }
472
473 /**
474 * Check if the CF notice is ready to be shown.
475 *
476 * @return bool
477 */
478 public static function is_cf_notice_ready(): bool {
479 return self::is_switched_ip_detection_notice( self::IP_DETECTION_CF_SHOW_SLUG )
480 && ! self::is_switched_ip_detection_notice( self::IP_DETECTION_CF_DISMISS_SLUG )
481 && ! wd_di()->get( Smart_Ip_Detection::class )->is_smart_ip_detection_enabled();
482 }
483
484 /**
485 * Delete all notice slugs related to IP detection switching.
486 */
487 public static function delete_slugs(): void {
488 delete_site_option( self::IP_DETECTION_CF_SHOW_SLUG );
489 delete_site_option( self::IP_DETECTION_CF_DISMISS_SLUG );
490 delete_site_option( self::IP_DETECTION_XFF_SHOW_SLUG );
491 delete_site_option( self::IP_DETECTION_XFF_DISMISS_SLUG );
492 }
493
494 /**
495 * Get the first blocked IP.
496 *
497 * @param array $ips The array of IPs to check.
498 *
499 * @return string
500 * @throws InvalidDatabaseException Thrown for unexpected data is found in DB.
501 */
502 public function get_blocked_ip( $ips ): string {
503 $blocked_ip = '';
504 foreach ( $ips as $ip ) {
505 $is_blocklisted = $this->is_blocklisted_ip( $ip );
506 if ( $is_blocklisted['result'] ) {
507 $blocked_ip = $ip;
508 break;
509 }
510 }
511 // Do not continue if there is not a single blocked IP.
512 if ( '' === $blocked_ip ) {
513 // Maybe IP(-s) in Active lockouts?
514 if ( count( $ips ) > 1 ) {
515 $models = Lockout_Ip::get_bulk( Lockout_Ip::STATUS_BLOCKED, $ips );
516 foreach ( $models as $model ) {
517 $blocked_ip = $model->ip;
518 break;
519 }
520 } elseif ( null !== Lockout_Ip::is_blocklisted_ip( $ips[0] ) ) {
521 $blocked_ip = $ips[0];
522 }
523 }
524
525 return $blocked_ip;
526 }
527
528 /**
529 * Get custom HTTP headers used for IP detection.
530 *
531 * @return array List of custom HTTP headers.
532 */
533 public static function custom_http_headers(): array {
534 return array(
535 'HTTP_X_FORWARDED_FOR',
536 'HTTP_X_REAL_IP',
537 'HTTP_CF_CONNECTING_IP',
538 );
539 }
540
541 /**
542 * Get trusted proxy presets.
543 *
544 * @return array List of trusted proxy presets.
545 */
546 public static function trusted_proxy_presets(): array {
547 return array(
548 Cloudflare_Proxy::PROXY_SLUG => esc_html__( 'Cloudflare', 'defender-security' ),
549 );
550 }
551
552 /**
553 * Dismiss the CF notice if the IP Detection is set to automatic.
554 */
555 public function maybe_dismiss_cf_notice(): void {
556 if (
557 wd_di()->get( Smart_Ip_Detection::class )->is_smart_ip_detection_enabled()
558 && self::is_switched_ip_detection_notice( self::IP_DETECTION_CF_SHOW_SLUG )
559 && ! self::is_switched_ip_detection_notice( self::IP_DETECTION_CF_DISMISS_SLUG )
560 ) {
561 update_site_option( self::IP_DETECTION_CF_DISMISS_SLUG, true );
562 }
563 }
564
565 /**
566 * Is whitelist server public IP enabled.
567 *
568 * @return bool
569 * @since 5.0.2
570 */
571 public function is_whitelist_server_public_ip_enabled(): bool {
572 /**
573 * Filter to enable/disable fetching server public IP.
574 *
575 * @param bool $enable True to enable whitelist server public IP, false otherwise.
576 *
577 * @since 5.0.2
578 */
579 $enabled = apply_filters( 'wpdef_firewall_whitelist_server_public_ip_enabled', true );
580 return is_bool( $enabled ) ? $enabled : (bool) $enabled;
581 }
582
583 /**
584 * Set whitelist server public IP.
585 *
586 * @return bool
587 * @since 5.0.2
588 */
589 public function set_whitelist_server_public_ip(): bool {
590 if ( ! $this->is_whitelist_server_public_ip_enabled() ) {
591 return false;
592 }
593
594 $ip = wd_di()->get( Smart_Ip_Detection::class )->get_server_public_ip();
595 if ( '' === $ip ) {
596 $this->log( 'Failed to whitelist server public IP.', Firewall_Controller::FIREWALL_LOG );
597 return false;
598 }
599
600 $stored_ip = $this->get_whitelist_server_public_ip();
601 if ( $stored_ip !== $ip ) {
602 update_site_option( self::WHITELIST_SERVER_PUBLIC_IP_OPTION, $ip );
603 }
604
605 $this->log( 'Server public IP whitelisted successfully.', Firewall_Controller::FIREWALL_LOG );
606 return true;
607 }
608
609 /**
610 * Get whitelist server public IP.
611 *
612 * @return string
613 * @since 5.0.2
614 */
615 public function get_whitelist_server_public_ip(): string {
616 return $this->is_whitelist_server_public_ip_enabled() ?
617 get_site_option( self::WHITELIST_SERVER_PUBLIC_IP_OPTION, '' ) :
618 '';
619 }
620 }
621