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

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

618 lines 16.8 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 ( isset( $server[ $header ] ) && '' !== $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 = 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 $gathered_ips = ! is_array( $gathered_ips ) ? (array) $gathered_ips : $gathered_ips;
359
360 return $this->filter_user_ips( $gathered_ips );
361 }
362
363 /**
364 * Check if the current request is recognized as coming from Cloudflare.
365 *
366 * @return bool
367 */
368 public function is_cloudflare_request(): bool {
369 $is_cloudflare = true;
370
371 $server = defender_get_data_from_request( null, 's' );
372 if ( ! (
373 isset( $server['HTTP_CF_CONNECTING_IP'] ) ||
374 isset( $server['HTTP_CF_IPCOUNTRY'] ) ||
375 isset( $server['HTTP_CF_RAY'] ) ||
376 isset( $server['HTTP_CF_VISITOR'] )
377 ) ) {
378 $is_cloudflare = false;
379 }
380
381 return $is_cloudflare;
382 }
383
384 /**
385 * Auto-detect proxy server and switch to appropriate IP Detection option.
386 *
387 * @return void
388 */
389 public function auto_switch_ip_detection_option(): void {
390 $model = wd_di()->get( Model_Firewall::class );
391
392 if ( $this->is_cloudflare_request() ) {
393 if (
394 'HTTP_CF_CONNECTING_IP' !== $model->http_ip_header &&
395 ! self::is_switched_ip_detection_notice( self::IP_DETECTION_CF_SHOW_SLUG )
396 ) {
397 $model->http_ip_header = 'HTTP_CF_CONNECTING_IP';
398 update_site_option( self::IP_DETECTION_CF_SHOW_SLUG, true );
399 }
400
401 $model->trusted_proxy_preset = Cloudflare_Proxy::PROXY_SLUG;
402 $model->save();
403
404 // Fetch trusted proxy ips.
405 $this->update_trusted_proxy_preset_ips();
406 }
407 }
408
409 /**
410 * Update trusted proxy preset IPs.
411 *
412 * @return void
413 */
414 public function update_trusted_proxy_preset_ips(): void {
415 $model = wd_di()->get( Model_Firewall::class );
416 if ( '' !== $model->trusted_proxy_preset ) {
417 /**
418 * Retrieve Trusted_Proxy_Preset instance.
419 *
420 * @var Trusted_Proxy_Preset $trusted_proxy_preset
421 */
422 $trusted_proxy_preset = wd_di()->get( Trusted_Proxy_Preset::class );
423 $trusted_proxy_preset->set_proxy_preset( $model->trusted_proxy_preset );
424 $trusted_proxy_preset->update_ips();
425 }
426 }
427
428 /**
429 * Show a notice if wrong IP Detection option is configured for the site.
430 *
431 * @return void
432 */
433 public function maybe_show_misconfigured_ip_detection_option_notice(): void {
434 $model = wd_di()->get( Model_Firewall::class );
435 $xff = defender_get_data_from_request( 'HTTP_X_FORWARDED_FOR', 's' );
436
437 if (
438 'HTTP_X_FORWARDED_FOR' !== $model->http_ip_header &&
439 ( is_string( $xff ) && 0 < strlen( $xff ) ) &&
440 ! $this->is_cloudflare_request() &&
441 ! self::is_xff_notice_ready()
442 ) {
443 update_site_option( self::IP_DETECTION_XFF_SHOW_SLUG, true );
444 }
445 }
446
447 /**
448 * Check if a switched IP detection notice has been shown.
449 *
450 * @param string $key The notice key to check.
451 *
452 * @return bool True if the notice has been shown, false otherwise.
453 */
454 public static function is_switched_ip_detection_notice( string $key ): bool {
455 return (bool) get_site_option( $key );
456 }
457
458 /**
459 * Check if the XFF notice is ready to be shown.
460 *
461 * @return bool
462 */
463 public static function is_xff_notice_ready(): bool {
464 return ! Onboard::maybe_show_onboarding() &&
465 self::is_switched_ip_detection_notice( self::IP_DETECTION_XFF_SHOW_SLUG )
466 && ! self::is_switched_ip_detection_notice( self::IP_DETECTION_XFF_DISMISS_SLUG )
467 && ! wd_di()->get( Smart_Ip_Detection::class )->is_smart_ip_detection_enabled()
468 && 'HTTP_X_FORWARDED_FOR' !== wd_di()->get( Model_Firewall::class )->http_ip_header;
469 }
470
471 /**
472 * Check if the CF notice is ready to be shown.
473 *
474 * @return bool
475 */
476 public static function is_cf_notice_ready(): bool {
477 return self::is_switched_ip_detection_notice( self::IP_DETECTION_CF_SHOW_SLUG )
478 && ! self::is_switched_ip_detection_notice( self::IP_DETECTION_CF_DISMISS_SLUG )
479 && ! wd_di()->get( Smart_Ip_Detection::class )->is_smart_ip_detection_enabled();
480 }
481
482 /**
483 * Delete all notice slugs related to IP detection switching.
484 */
485 public static function delete_slugs(): void {
486 delete_site_option( self::IP_DETECTION_CF_SHOW_SLUG );
487 delete_site_option( self::IP_DETECTION_CF_DISMISS_SLUG );
488 delete_site_option( self::IP_DETECTION_XFF_SHOW_SLUG );
489 delete_site_option( self::IP_DETECTION_XFF_DISMISS_SLUG );
490 }
491
492 /**
493 * Get the first blocked IP.
494 *
495 * @param array $ips The array of IPs to check.
496 *
497 * @return string
498 * @throws InvalidDatabaseException Thrown for unexpected data is found in DB.
499 */
500 public function get_blocked_ip( $ips ): string {
501 $blocked_ip = '';
502 foreach ( $ips as $ip ) {
503 $is_blocklisted = $this->is_blocklisted_ip( $ip );
504 if ( $is_blocklisted['result'] ) {
505 $blocked_ip = $ip;
506 break;
507 }
508 }
509 // Do not continue if there is not a single blocked IP.
510 if ( '' === $blocked_ip ) {
511 // Maybe IP(-s) in Active lockouts?
512 if ( count( $ips ) > 1 ) {
513 $models = Lockout_Ip::get_bulk( Lockout_Ip::STATUS_BLOCKED, $ips );
514 foreach ( $models as $model ) {
515 $blocked_ip = $model->ip;
516 break;
517 }
518 } elseif ( null !== Lockout_Ip::is_blocklisted_ip( $ips[0] ) ) {
519 $blocked_ip = $ips[0];
520 }
521 }
522
523 return $blocked_ip;
524 }
525
526 /**
527 * Get custom HTTP headers used for IP detection.
528 *
529 * @return array List of custom HTTP headers.
530 */
531 public static function custom_http_headers(): array {
532 return array(
533 'HTTP_X_FORWARDED_FOR',
534 'HTTP_X_REAL_IP',
535 'HTTP_CF_CONNECTING_IP',
536 );
537 }
538
539 /**
540 * Get trusted proxy presets.
541 *
542 * @return array List of trusted proxy presets.
543 */
544 public static function trusted_proxy_presets(): array {
545 return array(
546 Cloudflare_Proxy::PROXY_SLUG => esc_html__( 'Cloudflare', 'defender-security' ),
547 );
548 }
549
550 /**
551 * Dismiss the CF notice if the IP Detection is set to automatic.
552 */
553 public function maybe_dismiss_cf_notice(): void {
554 if (
555 wd_di()->get( Smart_Ip_Detection::class )->is_smart_ip_detection_enabled()
556 && self::is_switched_ip_detection_notice( self::IP_DETECTION_CF_SHOW_SLUG )
557 && ! self::is_switched_ip_detection_notice( self::IP_DETECTION_CF_DISMISS_SLUG )
558 ) {
559 update_site_option( self::IP_DETECTION_CF_DISMISS_SLUG, true );
560 }
561 }
562
563 /**
564 * Is whitelist server public IP enabled.
565 *
566 * @return bool
567 * @since 5.0.2
568 */
569 public function is_whitelist_server_public_ip_enabled(): bool {
570 /**
571 * Filter to enable/disable fetching server public IP.
572 *
573 * @param bool $enable True to enable whitelist server public IP, false otherwise.
574 *
575 * @since 5.0.2
576 */
577 return (bool) apply_filters( 'wpdef_firewall_whitelist_server_public_ip_enabled', true );
578 }
579
580 /**
581 * Set whitelist server public IP.
582 *
583 * @return bool
584 * @since 5.0.2
585 */
586 public function set_whitelist_server_public_ip(): bool {
587 if ( ! $this->is_whitelist_server_public_ip_enabled() ) {
588 return false;
589 }
590
591 $ip = wd_di()->get( Smart_Ip_Detection::class )->get_server_public_ip();
592 if ( '' === $ip ) {
593 $this->log( 'Failed to whitelist server public IP.', Firewall_Controller::FIREWALL_LOG );
594 return false;
595 }
596
597 $stored_ip = $this->get_whitelist_server_public_ip();
598 if ( $stored_ip !== $ip ) {
599 update_site_option( self::WHITELIST_SERVER_PUBLIC_IP_OPTION, $ip );
600 }
601
602 $this->log( 'Server public IP whitelisted successfully.', Firewall_Controller::FIREWALL_LOG );
603 return true;
604 }
605
606 /**
607 * Get whitelist server public IP.
608 *
609 * @return string
610 * @since 5.0.2
611 */
612 public function get_whitelist_server_public_ip(): string {
613 return $this->is_whitelist_server_public_ip_enabled() ?
614 get_site_option( self::WHITELIST_SERVER_PUBLIC_IP_OPTION, '' ) :
615 '';
616 }
617 }
618