PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.0
Defender Security – Malware Scanner, Login Security & Firewall v6.2.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 6.2.0, at src/component/class-firewall.php

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