PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 2.11.8
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v2.11.8
3.0.0 2.11.12 2.11.11 2.11.10 2.11.9 2.11.7 2.11.8 2.11.6 2.11.5 2.11.4 2.11.3 2.11.1 2.11.2 2.11.0 2.10.5 2.10.4 2.10.3 2.10.2 2.10.1 2.10.0 2.9.9 2.9.8 2.9.6 2.9.7 2.9.5 All 88 releases
← All changes | admin/class-admin-ajax.php +135 -110 2.10.12.11.8 View file →
@@ -22,95 +22,18 @@
22 22 * AJAX: Apply preset
23 23 */
24 24 // ajax_apply_preset() is defined in class-admin.php directly (not in this trait)
25 25
26 - /**
27 - * AJAX: Clear lockouts
26 + /*
27 + * ajax_clear_lockouts(), ajax_clear_logs(), ajax_run_scan() and
28 + * ajax_test_headers() live in class-admin.php. Until 2.11.8 this trait
29 + * carried older copies of the four, and PHP runs the method of the class,
30 + * so the copies never ran: a fix written into one of them would have looked
31 + * applied and changed nothing. Removed after the audit of the admin surface
32 + * for 2.11.8 found them.
28 33 */
29 - public function ajax_clear_lockouts() {
30 - check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
31 34
32 - if ( ! current_user_can( 'manage_options' ) ) {
33 - wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
34 - }
35 -
36 - $ip = isset( $_POST['ip'] ) ? sanitize_text_field( wp_unslash( $_POST['ip'] ) ) : '';
37 -
38 - if ( ! empty( $ip ) ) {
39 - // Clear specific IP
40 - $result = $this->database->clear_lockout( $ip );
41 - } else {
42 - // Clear all
43 - $result = $this->database->clear_all_lockouts();
44 - }
45 -
46 - if ( $result ) {
47 - wp_send_json_success( __( 'Lockouts cleared.', 'vigilante' ) );
48 - } else {
49 - wp_send_json_error( __( 'Failed to clear lockouts.', 'vigilante' ) );
50 - }
51 - }
52 -
53 35 /**
54 - * AJAX: Clear logs
55 - */
56 - public function ajax_clear_logs() {
57 - check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
58 -
59 - if ( ! current_user_can( 'manage_options' ) ) {
60 - wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
61 - }
62 -
63 - $result = $this->activity_log->clear_all_logs();
64 -
65 - if ( $result ) {
66 - wp_send_json_success( __( 'Logs cleared.', 'vigilante' ) );
67 - } else {
68 - wp_send_json_error( __( 'Failed to clear logs.', 'vigilante' ) );
69 - }
70 - }
71 -
72 - /**
73 - * AJAX: Run file integrity scan
74 - */
75 - public function ajax_run_scan() {
76 - check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
77 -
78 - if ( ! current_user_can( 'manage_options' ) ) {
79 - wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
80 - }
81 -
82 - try {
83 - if ( ! class_exists( 'Vigilante_File_Integrity' ) ) {
84 - require_once VIGILANTE_PLUGIN_DIR . 'includes/class-file-integrity.php';
85 - }
86 -
87 - if ( ! $this->settings ) {
88 - wp_send_json_error( 'Settings not initialized' );
89 - }
90 -
91 - $activity_log = isset( $this->activity_log ) ? $this->activity_log : null;
92 - $database = isset( $this->database ) ? $this->database : null;
93 -
94 - $file_integrity = new Vigilante_File_Integrity( $this->settings, $database, $activity_log );
95 - $results = $file_integrity->run_scan();
96 -
97 - // Save results for display
98 - update_option( 'vigilante_last_integrity_scan', time() );
99 - update_option( 'vigilante_last_integrity_results', $results );
100 -
101 - wp_send_json_success( array(
102 - 'message' => __( 'Scan completed.', 'vigilante' ),
103 - 'results' => $results,
104 - ) );
105 - } catch ( Exception $e ) {
106 - wp_send_json_error( 'Exception: ' . $e->getMessage() );
107 - } catch ( Error $e ) {
108 - wp_send_json_error( 'PHP Error: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine() );
109 - }
110 - }
111 -
112 - /**
113 36 * AJAX: Approve a critical config file modification
114 37 *
115 38 * Updates the baseline hash for a single critical file (wp-config.php
116 39 * or .htaccess), accepting the current content as legitimate.
@@ -117,9 +40,23 @@
117 40 */
118 41 public function ajax_approve_critical_file() {
119 42 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
120 43
121 - if ( ! current_user_can( 'manage_options' ) ) {
44 + // Both approvable files, wp-config.php and the root .htaccess, belong
45 + // to the whole network, and since 2.11.3 so does the baseline that
46 + // records them. Approving a change to them is a network action, so on
47 + // a network it takes a network administrator: manage_options is held
48 + // by the administrator of every subsite.
49 + // Written with both calls in plain sight, following the recipe in
50 + // native-aeo-pack/trunk/includes/class-robots-txt.php:650, so the
51 + // surface inventory can read the capability. With the name in a
52 + // variable it can only say "check by hand", and an alert that says
53 + // that forever is an alert nobody reads.
54 + $allowed = is_multisite()
55 + ? current_user_can( 'manage_network_options' )
56 + : current_user_can( 'manage_options' );
57 +
58 + if ( ! $allowed ) {
122 59 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
123 60 }
124 61
125 62 // The request carries an opaque key instead of the file name: hosting
@@ -243,8 +180,9 @@
243 180 $log->is_ip_whitelisted = ( '' !== $ip_val && in_array( $ip_val, $ip_whitelist, true ) );
244 181 $log->is_ip_blacklisted = ( '' !== $ip_val && in_array( $ip_val, $ip_blacklist, true ) );
245 182 $log->is_ua_whitelisted = ( '' !== $ua_val && in_array( $ua_val, $ua_whitelist, true ) );
246 183 $log->is_ua_blacklisted = ( '' !== $ua_val && in_array( $ua_val, $ua_blacklist, true ) );
184 + $log->request_uri = Vigilante_Activity_Log::extract_request_uri( $log->extra_data ?? '' );
247 185 }
248 186
249 187 wp_send_json_success( array(
250 188 'logs' => $logs,
@@ -325,8 +263,18 @@
325 263 $removed_from_opposite = true;
326 264 }
327 265 }
328 266
267 + // On the main site of a network the whitelists also build the .htaccess
268 + // rules every site shares, so a user without network rights cannot put
269 + // an entry in them or take one out (2.11.6).
270 + $locked = Vigilante_Settings::get_locked_file_settings();
271 + $locked_firewall = ( isset( $locked['firewall'] ) && is_array( $locked['firewall'] ) ) ? $locked['firewall'] : array();
272 +
273 + if ( in_array( $option_key, $locked_firewall, true ) || ( $removed_from_opposite && in_array( $opposite_key, $locked_firewall, true ) ) ) {
274 + wp_send_json_error( Vigilante_Settings::get_shared_files_notice() );
275 + }
276 +
329 277 wp_cache_delete( Vigilante_Settings::OPTION_NAME, 'options' );
330 278 update_option( Vigilante_Settings::OPTION_NAME, $all_options );
331 279 $this->settings->clear_cache();
332 280
@@ -375,24 +323,8 @@
375 323 wp_send_json_success( $message );
376 324 }
377 325
378 326 /**
379 - * AJAX: Test security headers
380 - */
381 - public function ajax_test_headers() {
382 - check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
383 -
384 - if ( ! current_user_can( 'manage_options' ) ) {
385 - wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
386 - }
387 -
388 - $security_headers = new Vigilante_Security_Headers( $this->settings );
389 - $results = $security_headers->test_headers();
390 -
391 - wp_send_json_success( $results );
392 - }
393 -
394 - /**
395 327 * Sanitize activity log data
396 328 *
397 329 * @param array $data Data to sanitize.
398 330 * @return array
@@ -627,21 +559,45 @@
627 559 if ( ! class_exists( 'Vigilante_Two_Factor_TOTP' ) ) {
628 560 require_once VIGILANTE_INCLUDES_DIR . 'class-two-factor-totp.php';
629 561 }
630 562
631 - $totp = new Vigilante_Two_Factor_TOTP( $this->settings, $this->database, $this->activity_log );
632 - $count = 0;
563 + $totp = new Vigilante_Two_Factor_TOTP( $this->settings, $this->database, $this->activity_log );
564 + $count = 0;
565 + $skipped = 0;
633 566
634 567 foreach ( $user_ids as $uid ) {
635 - if ( $uid > 0 ) {
636 - $totp->reset_user_totp( $uid );
637 - $count++;
568 + if ( $uid < 1 ) {
569 + continue;
638 570 }
571 +
572 + // Same gate as the rest of the TOTP handlers: resetting somebody's
573 + // second factor is editing their account, so ask for edit_user
574 + // rather than for manage_options, which on a network is per site.
575 + if ( ! current_user_can( 'edit_user', $uid ) ) {
576 + $skipped++;
577 + continue;
578 + }
579 +
580 + $totp->reset_user_totp( $uid );
581 + $count++;
639 582 }
640 583
584 + $message = sprintf(
585 + /* translators: %d: Number of users reset */
586 + _n( 'TOTP reset for %d user.', 'TOTP reset for %d users.', $count, 'vigilante' ),
587 + $count
588 + );
589 +
590 + if ( $skipped > 0 ) {
591 + $message .= ' ' . sprintf(
592 + /* translators: %d: Number of users skipped because the current user cannot edit them */
593 + __( '%d skipped: you cannot edit those users.', 'vigilante' ),
594 + $skipped
595 + );
596 + }
597 +
641 598 wp_send_json_success( array(
642 - /* translators: %d: Number of users reset */
643 - 'message' => sprintf( _n( 'TOTP reset for %d user.', 'TOTP reset for %d users.', $count, 'vigilante' ), $count ),
599 + 'message' => $message,
644 600 'count' => $count,
645 601 ) );
646 602 }
647 603
@@ -661,10 +617,11 @@
661 617 if ( 0 === $user_id ) {
662 618 wp_send_json_error( __( 'Invalid user.', 'vigilante' ) );
663 619 }
664 620
665 - // Permission check: own profile or admin
666 - if ( get_current_user_id() !== $user_id && ! current_user_can( 'manage_options' ) ) {
621 + // Permission check: own profile, or a user this one may actually edit.
622 + // manage_options is held by every subsite administrator on a network.
623 + if ( get_current_user_id() !== $user_id && ! current_user_can( 'edit_user', $user_id ) ) {
667 624 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
668 625 }
669 626
670 627 if ( ! class_exists( 'Vigilante_Two_Factor_TOTP' ) ) {
@@ -879,8 +836,16 @@
879 836 $results['failed']
880 837 );
881 838 }
882 839
840 + if ( ! empty( $results['skipped'] ) ) {
841 + $message .= ' ' . sprintf(
842 + /* translators: %d: Number of users skipped because the current user cannot edit them */
843 + __( '%d skipped: you cannot edit those users.', 'vigilante' ),
844 + $results['skipped']
845 + );
846 + }
847 +
883 848 wp_send_json_success( array(
884 849 'message' => $message,
885 850 'results' => $results,
886 851 'resetting_self' => $resetting_self,
@@ -941,8 +906,16 @@
941 906 $results['failed']
942 907 );
943 908 }
944 909
910 + if ( ! empty( $results['skipped'] ) ) {
911 + $message .= ' ' . sprintf(
912 + /* translators: %d: Number of users skipped because the current user cannot edit them */
913 + __( '%d skipped: you cannot edit those users.', 'vigilante' ),
914 + $results['skipped']
915 + );
916 + }
917 +
945 918 wp_send_json_success( array(
946 919 'message' => $message,
947 920 'results' => $results,
948 921 'resetting_self' => $include_self,
@@ -1035,8 +1008,16 @@
1035 1008 $results['failed']
1036 1009 );
1037 1010 }
1038 1011
1012 + if ( ! empty( $results['skipped'] ) ) {
1013 + $message .= ' ' . sprintf(
1014 + /* translators: %d: Number of users skipped because the current user cannot edit them */
1015 + __( '%d skipped: you cannot edit those users.', 'vigilante' ),
1016 + $results['skipped']
1017 + );
1018 + }
1019 +
1039 1020 // Check if current user was included via role membership.
1040 1021 $resetting_self = false;
1041 1022 if ( $include_self ) {
1042 1023 $current_user = wp_get_current_user();
@@ -1065,8 +1046,16 @@
1065 1046 if ( ! $user_id ) {
1066 1047 wp_send_json_error( __( 'Invalid user ID.', 'vigilante' ) );
1067 1048 }
1068 1049
1050 + // The pending flag is a user meta, shared by every site of a network, and
1051 + // approving opens the login everywhere. Same rule the other account tools
1052 + // got in 2.10.3: permission over that user, which on a network only a
1053 + // network administrator has (2.11.8).
1054 + if ( ! current_user_can( 'edit_user', $user_id ) ) {
1055 + wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1056 + }
1057 +
1069 1058 $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
1070 1059 $result = $user_security->approve_user( $user_id, get_current_user_id() );
1071 1060
1072 1061 if ( $result ) {
@@ -1099,8 +1088,14 @@
1099 1088 if ( ! $user_id ) {
1100 1089 wp_send_json_error( __( 'Invalid user ID.', 'vigilante' ) );
1101 1090 }
1102 1091
1092 + // See ajax_approve_user(): the account and its pending flag belong to the
1093 + // whole network (2.11.8).
1094 + if ( ! current_user_can( 'edit_user', $user_id ) ) {
1095 + wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1096 + }
1097 +
1103 1098 $user = get_userdata( $user_id );
1104 1099 $username = $user ? $user->user_login : $user_id;
1105 1100
1106 1101 $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
@@ -1139,8 +1134,14 @@
1139 1134 if ( ! $user ) {
1140 1135 wp_send_json_error( __( 'User not found.', 'vigilante' ) );
1141 1136 }
1142 1137
1138 + // Sessions carry IP, User-Agent and login time. manage_options alone is
1139 + // not enough on a network, where it is held per subsite.
1140 + if ( ! current_user_can( 'edit_user', $user_id ) ) {
1141 + wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1142 + }
1143 +
1143 1144 $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
1144 1145 $sessions = $user_security->get_user_sessions( $user_id );
1145 1146
1146 1147 wp_send_json_success( array(
@@ -1169,8 +1170,12 @@
1169 1170 if ( ! $user_id || ! $token_hash ) {
1170 1171 wp_send_json_error( __( 'Invalid parameters.', 'vigilante' ) );
1171 1172 }
1172 1173
1174 + if ( ! current_user_can( 'edit_user', $user_id ) ) {
1175 + wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1176 + }
1177 +
1173 1178 $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
1174 1179 $result = $user_security->revoke_session( $user_id, $token_hash );
1175 1180
1176 1181 if ( $result ) {
@@ -1198,8 +1203,12 @@
1198 1203 if ( ! $user_id ) {
1199 1204 wp_send_json_error( __( 'Invalid user ID.', 'vigilante' ) );
1200 1205 }
1201 1206
1207 + if ( ! current_user_can( 'edit_user', $user_id ) ) {
1208 + wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1209 + }
1210 +
1202 1211 $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
1203 1212 $count = $user_security->revoke_all_sessions( $user_id, $include_current );
1204 1213
1205 1214 wp_send_json_success( array(
@@ -1297,8 +1306,16 @@
1297 1306 if ( ! current_user_can( 'manage_options' ) ) {
1298 1307 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
1299 1308 }
1300 1309
1310 + // The dump is taken with $wpdb->prefix, which on the main site of a
1311 + // network matches every subsite table plus the global user tables, and
1312 + // the options it carries include the stored copy of wp-config.php. Same
1313 + // gate the rest of the network-shared operations use.
1314 + if ( ! Vigilante_Settings::can_write_shared_files() ) {
1315 + wp_send_json_error( Vigilante_Settings::get_shared_files_notice() );
1316 + }
1317 +
1301 1318 $backup = new Vigilante_Database_Backup();
1302 1319 $tables = $backup->get_tables();
1303 1320
1304 1321 wp_send_json_success( $tables );
@@ -1313,8 +1330,16 @@
1313 1330 check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
1314 1331
1315 1332 if ( ! current_user_can( 'manage_options' ) ) {
1316 1333 wp_die( esc_html__( 'Permission denied.', 'vigilante' ), 403 );
1334 + }
1335 +
1336 + // The dump is taken with $wpdb->prefix, which on the main site of a
1337 + // network matches every subsite table plus the global user tables, and
1338 + // the options it carries include the stored copy of wp-config.php. Same
1339 + // gate the rest of the network-shared operations use.
1340 + if ( ! Vigilante_Settings::can_write_shared_files() ) {
1341 + wp_die( esc_html( Vigilante_Settings::get_shared_files_notice() ), 403 );
1317 1342 }
1318 1343
1319 1344 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
1320 1345 $tables_raw = isset( $_POST['tables'] ) ? wp_unslash( $_POST['tables'] ) : '';