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

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