PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.3.0
Defender Security – Malware Scanner, Login Security & Firewall v6.3.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
← All changes | src/controller/class-security-tweaks.php +217 -59 5.7.0 → 6.3.0 View file →
@@ -8,12 +8,12 @@
8 8 namespace WP_Defender\Controller;
9 9
10 10 use Countable;
11 11 use WP_Defender\Event;
12 -use WP_Defender\Admin;
13 12 use Calotes\Component\Request;
14 13 use Calotes\Component\Response;
15 14 use Calotes\Helper\Array_Cache;
15 +use WP_Defender\Behavior\WPMUDEV;
16 16 use WP_Defender\Model\Notification\Tweak_Reminder;
17 17 use WP_Defender\Component\Config\Config_Hub_Helper;
18 18 use WP_Defender\Component\Security_Tweaks\Hide_Error;
19 19 use WP_Defender\Component\Security_Tweaks\WP_Version;
@@ -72,13 +72,33 @@
72 72
73 73 public const STATUS_ISSUES = 'issues', STATUS_RESOLVE = 'fixed', STATUS_IGNORE = 'ignore', STATUS_RESTORE = 'restore';
74 74
75 75 /**
76 + * Maps tweak slugs to frontend category identifiers.
77 + *
78 + * @var array<string,string>
79 + */
80 + private const TWEAK_CATEGORIES = array(
81 + 'disable-xml-rpc' => 'network',
82 + 'wp-version' => 'updates',
83 + 'hide-error' => 'error',
84 + 'php-version' => 'updates',
85 + 'replace-admin-username' => 'auth',
86 + 'security-key' => 'updates',
87 + 'login-duration' => 'auth',
88 + 'disable-trackback' => 'network',
89 + 'prevent-enum-users' => 'error',
90 + 'disable-file-editor' => 'access',
91 + 'protect-information' => 'error',
92 + 'prevent-php-executed' => 'access',
93 + );
94 +
95 + /**
76 96 * Initializes the model and service, registers routes, and sets up scheduled events if the model is active.
77 97 */
78 98 public function __construct() {
79 99 $this->register_page(
80 - esc_html__( 'Recommendations', 'defender-security' ),
100 + esc_html__( 'Hardening', 'defender-security' ),
81 101 $this->slug,
82 102 array( $this, 'main_view' ),
83 103 $this->parent_slug
84 104 );
@@ -101,19 +121,20 @@
101 121 /**
102 122 * Dummy function for testing a check.
103 123 */
104 124 public function should_output_error() {
105 - if ( empty( defender_get_data_from_request( 'defender_test_error_reporting', 'g' ) ) ) {
125 + $test_err_reporting = defender_get_data_from_request( 'defender_test_error_reporting', 'g' );
126 + if ( ! is_string( $test_err_reporting ) || '' === trim( $test_err_reporting ) ) {
106 127 return;
107 128 }
108 - // It should be only trigger by admin.
129 + // It should be triggered only by admin.
109 130 if ( ! $this->check_permission() ) {
110 131 return;
111 132 }
112 133
113 - $var = '$' . uniqid( '', true );
114 - // This should output a warning. Ignored phpcs as it's a test.
115 - echo ${$var}; // phpcs:ignore
134 + // This should output a warning. Ignored by PHPCS as it's a test.
135 + // @phpstan-ignore-next-line.
136 + echo $undefined_variable; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Intentionally outputs an undefined variable to test warning handling.
116 137 exit();
117 138 }
118 139
119 140 /**
@@ -134,8 +155,12 @@
134 155 'current_server' => array(
135 156 'type' => 'string',
136 157 'sanitize' => 'sanitize_text_field',
137 158 ),
159 + 'username' => array(
160 + 'type' => 'string',
161 + 'sanitize' => 'sanitize_user',
162 + ),
138 163 )
139 164 );
140 165
141 166 $slug = $data['slug'] ?? false;
@@ -147,8 +172,39 @@
147 172 array( 'message' => esc_html__( 'Invalid request', 'defender-security' ) )
148 173 );
149 174 }
150 175
176 + if ( 'prevent-enum-users' === $slug ) {
177 + $pe_data = $request->get_data();
178 + $enabled_enums = $pe_data['enabled_user_enums'] ?? null;
179 + $all_enums = array( 'enum-rest', 'enum-oembed', 'enum-sitemap' );
180 + $this->prevent_enum_users->set_enabled_user_enums(
181 + is_array( $enabled_enums ) ? $enabled_enums : $all_enums
182 + );
183 + }
184 +
185 + if ( 'security-key' === $slug ) {
186 + $sk_data = $request->get_data();
187 + $remind_date = sanitize_text_field( $sk_data['remind_date'] ?? '' );
188 + if ( $remind_date ) {
189 + $is_autogen = filter_var( $sk_data['is_autogenerate_keys'] ?? false, FILTER_VALIDATE_BOOLEAN );
190 + $values = get_site_option( 'defender_security_tweaks_' . $this->security_key->slug, array() );
191 + $values['reminder_duration'] = $remind_date;
192 + $values['reminder_date'] = strtotime( '+' . $remind_date, time() );
193 + $values['is_autogenerate_keys'] = $is_autogen;
194 + update_site_option( 'defender_security_tweaks_' . $this->security_key->slug, $values );
195 + if ( true === $is_autogen ) {
196 + $this->security_key->cron_schedule();
197 + } else {
198 + $this->security_key->cron_unschedule();
199 + }
200 + }
201 + }
202 +
203 + if ( 'replace-admin-username' === $slug && isset( $data['username'] ) && '' !== trim( $data['username'] ) ) {
204 + $_POST['username'] = $data['username'];
205 + }
206 +
151 207 if ( in_array( $slug, array( 'prevent-php-executed', 'protect-information' ), true ) ) {
152 208 $current_server = $data['current_server'] ?? false;
153 209 if ( ! $current_server ) {
154 210 return new Response(
@@ -169,8 +225,14 @@
169 225 $this->track_tweak( $tweak->get_label(), 'Actioned' );
170 226 // Response.
171 227 $this->ajax_response( esc_html__( 'Security recommendation successfully resolved.', 'defender-security' ) );
172 228 }
229 + if ( $ret instanceof Response && true === $ret->status ) {
230 + Config_Hub_Helper::set_clear_active_flag();
231 + $this->model->mark( self::STATUS_RESOLVE, $slug );
232 + $this->track_tweak( $tweak->get_label(), 'Actioned' );
233 + return $ret;
234 + }
173 235 if ( is_wp_error( $ret ) ) {
174 236 $this->ajax_response( $ret->get_error_message(), false );
175 237 }
176 238
@@ -418,8 +480,37 @@
418 480 }
419 481 }
420 482
421 483 /**
484 + * Returns tweaks for a given status formatted for the React frontend.
485 + *
486 + * Adds `category` and remaps the backend status string to the value the
487 + * frontend HARDENING_STATUS constants expect ('issue', 'resolved', 'ignored').
488 + *
489 + * @param string $status One of STATUS_ISSUES, STATUS_RESOLVE, STATUS_IGNORE.
490 + *
491 + * @return array
492 + */
493 + private function format_tweaks_for_frontend( string $status ): array {
494 + $status_map = array(
495 + self::STATUS_ISSUES => 'issue',
496 + self::STATUS_RESOLVE => 'resolved',
497 + self::STATUS_IGNORE => 'ignored',
498 + );
499 +
500 + $tweaks = $this->init_tweaks( $status, 'array' );
501 + $frontend_status = $status_map[ $status ] ?? $status;
502 +
503 + foreach ( $tweaks as &$tweak ) {
504 + $tweak['status'] = $frontend_status;
505 + $tweak['category'] = self::TWEAK_CATEGORIES[ $tweak['slug'] ] ?? 'access';
506 + }
507 + unset( $tweak );
508 +
509 + return $tweaks;
510 + }
511 +
512 + /**
422 513 * AJAX Response handler.
423 514 *
424 515 * @param string $message The message to be displayed in the response.
425 516 * @param bool $is_success Whether the response should have a success status.
@@ -429,22 +520,23 @@
429 520 */
430 521 private function ajax_response( $message, $is_success = true, $interval = false ): Response {
431 522 global $wp_version;
432 523
433 - $settings = new Model_Security_Tweaks();
434 - $data = array(
524 + // Creating a new Model_Security_Tweaks() here risks reading stale data from the
525 + // DB when the WP options cache hasn't flushed the write yet.
526 + $data = array(
435 527 'message' => $message,
436 528 'summary' => array(
437 - 'issues_count' => count( $settings->issues ),
438 - 'fixed_count' => count( $settings->fixed ),
439 - 'ignore_count' => count( $settings->ignore ),
529 + 'issues_count' => count( $this->model->issues ),
530 + 'fixed_count' => count( $this->model->fixed ),
531 + 'ignore_count' => count( $this->model->ignore ),
440 532 'php_version' => PHP_VERSION,
441 533 'wp_version' => $wp_version,
442 534 ),
443 - 'issues' => $this->init_tweaks( self::STATUS_ISSUES, 'array' ),
444 - 'fixed' => $this->init_tweaks( self::STATUS_RESOLVE, 'array' ),
445 - 'ignored' => $this->init_tweaks( self::STATUS_IGNORE, 'array' ),
446 - 'issues_slugs' => $settings->issues,
535 + 'issues' => $this->build_tweaks_response( $this->model->issues, 'issue' ),
536 + 'fixed' => $this->build_tweaks_response( $this->model->fixed, 'resolved' ),
537 + 'ignored' => $this->build_tweaks_response( $this->model->ignore, 'ignored' ),
538 + 'issues_slugs' => $this->model->issues,
447 539 );
448 540 if ( $interval ) {
449 541 $data['interval'] = $interval;
450 542 }
@@ -452,8 +544,36 @@
452 544 return new Response( $is_success, $data );
453 545 }
454 546
455 547 /**
548 + * Builds the frontend-shaped tweak array from a list of slugs.
549 + *
550 + * Uses the in-memory tweak object cache rather than re-querying the model,
551 + * so the result is always consistent with the state after mark() was called.
552 + *
553 + * @param array $slugs Slugs to include.
554 + * @param string $frontend_status Frontend status string ('issue', 'resolved', 'ignored').
555 + *
556 + * @return array
557 + */
558 + private function build_tweaks_response( array $slugs, string $frontend_status ): array {
559 + $tweaks = Array_Cache::get( 'tweaks', 'tweaks' );
560 + $result = array();
561 +
562 + foreach ( $slugs as $slug ) {
563 + if ( ! isset( $tweaks[ $slug ] ) ) {
564 + continue;
565 + }
566 + $arr = $tweaks[ $slug ]->to_array();
567 + $arr['status'] = $frontend_status;
568 + $arr['category'] = self::TWEAK_CATEGORIES[ $slug ] ?? 'access';
569 + $result[ $slug ] = $arr;
570 + }
571 +
572 + return $result;
573 + }
574 +
575 + /**
456 576 * Enqueues scripts and styles for this page.
457 577 * Only enqueues assets if the page is active.
458 578 */
459 579 public function enqueue_assets() {
@@ -460,10 +580,34 @@
460 580 if ( ! $this->is_page_active() ) {
461 581 return;
462 582 }
463 583
464 - wp_localize_script( 'def-securitytweaks', 'security_tweaks', $this->data_frontend() );
465 - wp_enqueue_script( 'def-securitytweaks' );
584 + $handle = 'defender-ui-hardening';
585 + wp_enqueue_script(
586 + $handle,
587 + WP_DEFENDER_BASE_URL . 'assets/js/hardening-ui.js',
588 + array( 'def-vue', 'def-manifest', 'def-core-ui', 'defender', 'wp-i18n' ),
589 + DEFENDER_VERSION,
590 + true
591 + );
592 + wp_set_script_translations( $handle, 'wpdef' );
593 +
594 + wp_localize_script(
595 + $handle,
596 + 'defenderUIData',
597 + array_merge(
598 + $this->get_shared_data(),
599 + $this->data_frontend()
600 + )
601 + );
602 +
603 + wp_enqueue_style(
604 + $handle,
605 + WP_DEFENDER_BASE_URL . 'assets/css/core-ui.css',
606 + array(),
607 + DEFENDER_VERSION
608 + );
609 +
466 610 $this->enqueue_main_assets();
467 611 }
468 612
469 613 /**
@@ -510,9 +654,9 @@
510 654 }
511 655
512 656 $tweak_arr = $this->model->get_tweak_types();
513 657
514 - $data = array(
658 + $hardening_data = array(
515 659 'summary' => array(
516 660 'fixed_count' => $tweak_arr['count_fixed'],
517 661 'ignore_count' => $tweak_arr['count_ignored'],
518 662 'issues_count' => $tweak_arr['count_issues'],
@@ -518,21 +662,20 @@
518 662 'issues_count' => $tweak_arr['count_issues'],
519 663 'php_version' => PHP_VERSION,
520 664 'wp_version' => $wp_version,
521 665 ),
522 - // Todo: improve the logic so that Tweak initialization happens in one go and return 3 different types.
523 - 'issues' => $this->init_tweaks( self::STATUS_ISSUES, 'array' ),
524 - 'fixed' => $this->init_tweaks( self::STATUS_RESOLVE, 'array' ),
525 - 'ignored' => $this->init_tweaks( self::STATUS_IGNORE, 'array' ),
666 + 'issues' => $this->format_tweaks_for_frontend( self::STATUS_ISSUES ),
667 + 'fixed' => $this->format_tweaks_for_frontend( self::STATUS_RESOLVE ),
668 + 'ignored' => $this->format_tweaks_for_frontend( self::STATUS_IGNORE ),
526 669 'not_allowed_bulk' => $not_allowed_bulk,
527 670 'is_autogenerate_keys' => $this->security_key->get_is_autogenerate_keys(),
528 671 'reminder_frequencies' => $this->security_key->reminder_frequencies(),
529 672 'enabled_user_enums' => $this->prevent_enum_users->get_enabled_user_enums(),
530 - 'hub_connector' => wd_di()->get( Hub_Connector::class )->data_frontend(),
531 - 'antibot' => wd_di()->get( Antibot_Global_Firewall::class )->data_frontend(),
532 673 );
533 674
534 - return array_merge( $data, $this->dump_routes_and_nonces() );
675 + return array(
676 + 'hardening' => array_merge( $hardening_data, $this->dump_routes_and_nonces() ),
677 + );
535 678 }
536 679
537 680 /**
538 681 * Renders the main view for this page.
@@ -565,10 +708,14 @@
565 708 );
566 709 $slugs = $data['slugs'] ?? array();
567 710 $intention = $data['intention'] ?? false;
568 711 // Get processed and unprocessed tweaks.
569 - [ $processed, $unprocessed ] = $this->security_tweaks_auto_action( $slugs, $intention );
712 + [ $processed, $unprocessed, $error_message ] = $this->security_tweaks_auto_action( $slugs, $intention );
570 713
714 + if ( null !== $error_message ) {
715 + return $this->ajax_response( $error_message, false );
716 + }
717 +
571 718 $message = sprintf(
572 719 /* translators: 1: Either ignored or resolved, 2: Count security recommendations */
573 720 esc_html__( 'You have bulk %1$s %2$s security recommendations.', 'defender-security' ),
574 721 'ignore' === $intention ? 'ignored' : 'resolved',
@@ -600,10 +747,11 @@
600 747 *
601 748 * @return array
602 749 */
603 750 public function security_tweaks_auto_action( $slugs, $intention ): array {
604 - $processed = 0;
605 - $unprocessed = 0;
751 + $processed = 0;
752 + $unprocessed = 0;
753 + $error_message = null;
606 754
607 755 foreach ( $slugs as $slug ) {
608 756 $tweak = $this->get_tweak( $slug );
609 757 if ( 'ignore' === $intention ) {
@@ -627,29 +775,34 @@
627 775 } else {
628 776 $ret = $tweak->process();
629 777 }
630 778 if ( is_wp_error( $ret ) ) {
631 - $this->ajax_response(
632 - sprintf(
633 - /* translators: 1: Security tweak title, 2: Error message */
634 - esc_html__(
635 - 'There is an error while processing recommendation %1$s, error message: %2$s',
636 - 'defender-security'
637 - ),
638 - $tweak->get_label(),
639 - $ret->get_error_message()
779 + $error_message = sprintf(
780 + /* translators: 1: Security tweak title, 2: Error message */
781 + esc_html__(
782 + 'There is an error while processing recommendation %1$s, error message: %2$s',
783 + 'defender-security'
640 784 ),
641 - false
785 + $tweak->get_label(),
786 + $ret->get_error_message()
642 787 );
788 + ++$unprocessed;
789 + // Don't mark a failed tweak as resolved; stop on first error (same as original behavior).
790 + break;
643 791 }
644 792 $this->model->mark( self::STATUS_RESOLVE, $slug );
645 793 // Track.
646 794 $this->track_tweak( $tweak->get_label(), 'Actioned' );
795 + } elseif ( 'restore' === $intention ) {
796 + // New bulk action since V6.
797 + $this->model->mark( self::STATUS_ISSUES, $slug );
798 + // Track.
799 + $this->track_tweak( $tweak->get_label(), 'Restored' );
647 800 }
648 801 ++$processed;
649 802 }
650 803
651 - return array( $processed, $unprocessed );
804 + return array( $processed, $unprocessed, $error_message );
652 805 }
653 806
654 807 /**
655 808 * Refresh the tweak status and save their state.
@@ -733,13 +886,19 @@
733 886 }
734 887 Array_Cache::set( 'tweaks', $tweaks, 'tweaks' );
735 888 }
736 889 $tmp = array();
737 - if ( empty( $type ) ) {
890 + if ( ! is_string( $type ) || '' === trim( $type ) ) {
738 891 $tmp = $tweaks;
739 892 } else {
740 893 $settings = new Model_Security_Tweaks();
741 - $compare = $settings->$type;
894 +
895 + try {
896 + $compare = $settings->get_collection( $type );
897 + } catch ( \InvalidArgumentException $e ) {
898 + $compare = array();
899 + }
900 +
742 901 foreach ( $compare as $slug ) {
743 902 if ( isset( $tweaks[ $slug ] ) ) {
744 903 $tmp[ $slug ] = $tweaks[ $slug ];
745 904 }
@@ -833,9 +992,9 @@
833 992 public function automate( $data, $request_reason ) {
834 993 $this->refresh_tweaks_status();
835 994 $need_reauth = false;
836 995 // Resolve tweaks.
837 - if ( ! empty( $data['fixed'] ) ) {
996 + if ( isset( $data['fixed'] ) && is_array( $data['fixed'] ) && array() !== $data['fixed'] ) {
838 997 // There are some tweak that need manual apply, as files based, or change admin.
839 998 $manual_done = array(
840 999 'replace-admin-username',
841 1000 'prevent-php-executed',
@@ -847,9 +1006,9 @@
847 1006 $manual_done[] = 'security-key';
848 1007 }
849 1008
850 1009 $diff_keys = array_diff( $data['fixed'], $this->model->fixed, $manual_done );
851 - if ( ! empty( $diff_keys ) ) {
1010 + if ( array() !== $diff_keys ) {
852 1011 foreach ( $diff_keys as $slug ) {
853 1012 $tweak = $this->get_tweak( $slug );
854 1013 if ( $tweak->has_method( 'bulk_process' ) ) {
855 1014 $ret = $tweak->bulk_process();
@@ -880,12 +1039,12 @@
880 1039 }
881 1040 }
882 1041 }
883 1042 // Revert tweaks.
884 - if ( ! empty( $data['issues'] ) ) {
1043 + if ( isset( $data['issues'] ) && is_array( $data['issues'] ) && array() !== $data['issues'] ) {
885 1044 $diff_keys = array_diff( $data['issues'], $this->model->issues );
886 1045
887 - if ( ! empty( $diff_keys ) ) {
1046 + if ( array() !== $diff_keys ) {
888 1047 // Issues.
889 1048 foreach ( $diff_keys as $slug ) {
890 1049 $tweak = $this->get_tweak( $slug );
891 1050 $ret = $tweak->revert();
@@ -908,11 +1067,11 @@
908 1067 }
909 1068 }
910 1069 }
911 1070 // Ignore tweaks.
912 - if ( ! empty( $data['ignore'] ) ) {
1071 + if ( isset( $data['ignore'] ) && is_array( $data['ignore'] ) && array() !== $data['ignore'] ) {
913 1072 $diff_keys = array_diff( $data['ignore'], $this->model->ignore );
914 - if ( ! empty( $diff_keys ) ) {
1073 + if ( array() !== $diff_keys ) {
915 1074 foreach ( $diff_keys as $slug ) {
916 1075 $this->model->mark( self::STATUS_IGNORE, $slug );
917 1076 }
918 1077 }
@@ -936,9 +1095,9 @@
936 1095 }
937 1096
938 1097 $this->prevent_enum_users->set_enabled_user_enums( $enabled_user_enums );
939 1098
940 - if ( ! empty( $data['security_key'] ) && is_array( $data['security_key'] ) ) {
1099 + if ( isset( $data['security_key'] ) && is_array( $data['security_key'] ) && array() !== $data['security_key'] ) {
941 1100 $this->security_key->update_all_option( $data['security_key'] );
942 1101 }
943 1102
944 1103 $model = new Model_Security_Tweaks();
@@ -959,9 +1118,9 @@
959 1118 $settings = new Model_Security_Tweaks();
960 1119 $strings = array();
961 1120 $count_all = count( $settings->fixed ) + count( $settings->issues ) + count( $settings->ignore );
962 1121
963 - if ( empty( $settings->issues ) ) {
1122 + if ( ! is_array( $settings->issues ) || array() === $settings->issues ) {
964 1123 $strings[] = esc_html__( 'All available recommendations activated', 'defender-security' );
965 1124 } else {
966 1125 $strings[] = sprintf(
967 1126 /* translators: 1: Total security tweaks activated, 2: Total security tweaks */
@@ -970,13 +1129,8 @@
970 1129 $count_all
971 1130 );
972 1131 }
973 1132
974 - $tweak_notification = new Tweak_Reminder();
975 - if ( 'enabled' === $tweak_notification->status ) {
976 - $strings[] = esc_html__( 'Email notifications active', 'defender-security' );
977 - }
978 -
979 1133 return $strings;
980 1134 }
981 1135
982 1136 /**
@@ -982,15 +1136,18 @@
982 1136 /**
983 1137 * Config strings.
984 1138 *
985 1139 * @param array $config Settings.
986 - * @param bool $is_pro True if it is a pro version.
987 1140 *
988 1141 * @return array Strings to be displayed in the settings page.
989 1142 */
990 - public function config_strings( $config, $is_pro ): array {
1143 + public function config_strings( $config ): array {
991 1144 $strings = array();
992 - if ( empty( $config['issues'] ) ) {
1145 + if (
1146 + ! isset( $config['issues'] )
1147 + || ! ( is_array( $config['issues'] ) || $config['issues'] instanceof Countable )
1148 + || 0 === count( $config['issues'] )
1149 + ) {
993 1150 $strings[] = esc_html__( 'All available recommendations activated', 'defender-security' );
994 1151 } else {
995 1152 $strings[] = sprintf(
996 1153 /* translators: 1: Total security tweaks activated, 2: Total security tweaks */
@@ -1060,9 +1217,10 @@
1060 1217 * @return Response
1061 1218 * @defender_route
1062 1219 */
1063 1220 public function update_enabled_user_enums( Request $request ): Response {
1064 - $data = (array) $request->get_data();
1221 + $data = $request->get_data();
1222 + $data = ! is_array( $data ) ? (array) $data : $data;
1065 1223 $enabled_user_enums = $data['enabled_user_enums'];
1066 1224 $is_success = false;
1067 1225 $message = esc_html__( 'An error occurred, try again.', 'defender-security' );
1068 1226