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-firewall.php

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

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