PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 3.0.2
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v3.0.2
3.0.5 3.0.4 3.0.3 3.0.2 3.0.1 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 All 93 releases
← All changes | admin/class-admin.php +92 -11 3.0.1 → 3.0.2 View file →
@@ -1008,8 +1008,28 @@
1008 1008 return count( $pending_users );
1009 1009 }
1010 1010
1011 1011 /**
1012 + * Whether XML-RPC is blocked, whole or its pingback methods
1013 + *
1014 + * Both levels have to say so: the three-way setting under WP Hardening, read
1015 + * through the module's own resolver, and the WP Hardening module that
1016 + * enforces it. Until 3.0.2 the Dashboard read login_security[disable_xmlrpc],
1017 + * the checkbox that moved to WP Hardening in 2.9.7 and is never written any
1018 + * more, so the score never granted its three points and the recommendation
1019 + * to disable XML-RPC stayed on for good.
1020 + *
1021 + * @since 3.0.2
1022 + *
1023 + * @param array $options Plugin options.
1024 + * @return bool
1025 + */
1026 + private function is_xmlrpc_protected( $options ) {
1027 + return ! empty( $options['modules']['wp_hardening'] )
1028 + && 'none' !== Vigilante_Comment_Security::resolve_xmlrpc_mode( $this->settings );
1029 + }
1030 +
1031 + /**
1012 1032 * Calculate comprehensive security score (0-100)
1013 1033 *
1014 1034 * @param array $options Plugin options.
1015 1035 * @return int Security score.
@@ -1067,10 +1087,10 @@
1067 1087 // Max attempts configured
1068 1088 if ( isset( $login['max_attempts'] ) && $login['max_attempts'] <= 5 ) {
1069 1089 $score += 3;
1070 1090 }
1071 - // XML-RPC disabled
1072 - if ( ! empty( $login['disable_xmlrpc'] ) ) {
1091 + // XML-RPC blocked, whole or its pingback methods
1092 + if ( $this->is_xmlrpc_protected( $options ) ) {
1073 1093 $score += 3;
1074 1094 }
1075 1095 // 2FA enabled
1076 1096 if ( ! empty( $login['two_factor']['enabled'] ) ) {
@@ -1310,14 +1330,14 @@
1310 1330 );
1311 1331 }
1312 1332
1313 1333 // Low: XML-RPC enabled (only if login security is active)
1314 - if ( ! empty( $options['modules']['login_security'] ) && empty( $login['disable_xmlrpc'] ) ) {
1334 + if ( ! empty( $options['modules']['login_security'] ) && ! $this->is_xmlrpc_protected( $options ) ) {
1315 1335 $recommendations[] = array(
1316 1336 'icon' => 'info',
1317 1337 'priority' => 'low',
1318 1338 'message' => __( 'Disable XML-RPC if not needed (reduces attack surface).', 'vigilante' ),
1319 - 'tab' => 'login',
1339 + 'tab' => 'wp-hardening',
1320 1340 );
1321 1341 }
1322 1342
1323 1343 // Low: Strong passwords not enforced (only if user security is active)
@@ -2703,9 +2723,9 @@
2703 2723 <span class="dashicons dashicons-shield-alt" aria-hidden="true"></span>
2704 2724 <?php esc_html_e( 'Security Check', 'vigilante' ); ?>
2705 2725 </h2>
2706 2726 <p class="description">
2707 - <?php esc_html_e( 'On-demand audit of what an attacker would see right now, plus 13 internal checks impossible from the outside.', 'vigilante' ); ?>
2727 + <?php esc_html_e( 'On-demand audit of what an attacker would see right now, plus internal checks impossible from the outside.', 'vigilante' ); ?>
2708 2728 </p>
2709 2729 <p class="vigilante-analyzer-last-scan" data-role="ran-at">
2710 2730 <span class="dashicons dashicons-clock" aria-hidden="true"></span>
2711 2731 <?php echo esc_html( $ran_at_human ); ?>
@@ -4773,8 +4793,9 @@
4773 4793 <label>
4774 4794 <input type="checkbox" name="security_headers[csp][enabled]" value="1" <?php checked( ! empty( $options['csp']['enabled'] ) ); ?>>
4775 4795 <?php esc_html_e( 'Enable Content Security Policy', 'vigilante' ); ?>
4776 4796 </label>
4797 + <p class="description"><?php esc_html_e( 'The default policy includes upgrade-insecure-requests, which asks browsers to request every http:// resource over https. Vigilant writes it only when the site address starts with https://, because on a site served over http it would stop every stylesheet, script and image from loading. If your site is served over https through a proxy or CDN that terminates TLS, set the site address to https:// in Settings > General; the rules follow the next time this tab is saved or Vigilant is updated.', 'vigilante' ); ?></p>
4777 4798 </td>
4778 4799 </tr>
4779 4800 <tr>
4780 4801 <th scope="row"><?php esc_html_e( 'Report Only Mode', 'vigilante' ); ?></th>
@@ -7794,9 +7815,9 @@
7794 7815 } elseif ( isset( $_POST['data'] ) ) {
7795 7816 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
7796 7817 $raw_data = wp_unslash( $_POST['data'] );
7797 7818 parse_str( $raw_data, $data );
7798 - $data = map_deep( $data, 'sanitize_textarea_field' );
7819 + $data = $this->sanitize_posted_settings( $data );
7799 7820 } else {
7800 7821 $data = array();
7801 7822 }
7802 7823
@@ -8186,8 +8207,48 @@
8186 8207 return array( 'sent' => $sent, 'failed' => $failed );
8187 8208 }
8188 8209
8189 8210 /**
8211 + * Sanitize the settings a tab posted, once the serialized form is parsed
8212 + *
8213 + * Every value goes through sanitize_textarea_field() except the lists of
8214 + * file paths (Vigilante_Settings::path_list_keys()). That one deletes every
8215 + * %XX octet and, when it finds one, collapses repeated spaces, so an
8216 + * exclusion for a folder called foto%20x was saved as fotox and excluded
8217 + * nothing (measured on 1 oct 2026 against 3.0.1). The paths are read as
8218 + * paths before the generic sanitizer runs and put back after it.
8219 + *
8220 + * @since 3.0.2
8221 + *
8222 + * @param array $data Parsed form: section => key => value.
8223 + * @return array
8224 + */
8225 + private function sanitize_posted_settings( $data ) {
8226 + $paths = Vigilante_Settings::take_path_lists( $data );
8227 + $data = map_deep( $data, 'sanitize_textarea_field' );
8228 +
8229 + return Vigilante_Settings::restore_path_lists( $data, $paths );
8230 + }
8231 +
8232 + /**
8233 + * Sanitize an imported settings file, once it is decoded
8234 + *
8235 + * Same rule as sanitize_posted_settings(): the lists of paths keep what the
8236 + * export wrote, so an exclusion survives an export and an import.
8237 + *
8238 + * @since 3.0.2
8239 + *
8240 + * @param array $imported Decoded export.
8241 + * @return array
8242 + */
8243 + private function sanitize_imported_settings( $imported ) {
8244 + $paths = Vigilante_Settings::take_path_lists( $imported );
8245 + $imported = map_deep( $imported, 'sanitize_text_field' );
8246 +
8247 + return Vigilante_Settings::restore_path_lists( $imported, $paths );
8248 + }
8249 +
8250 + /**
8190 8251 * Process section data maintaining proper types from defaults
8191 8252 *
8192 8253 * @param array $submitted_data Data submitted from form.
8193 8254 * @param array $defaults Default values for this section.
@@ -8514,9 +8575,9 @@
8514 8575 wp_send_json_error( __( 'Invalid JSON format.', 'vigilante' ) );
8515 8576 }
8516 8577
8517 8578 // Sanitize imported data recursively
8518 - $imported = map_deep( $imported, 'sanitize_text_field' );
8579 + $imported = $this->sanitize_imported_settings( $imported );
8519 8580
8520 8581 // Validate structure: only sections and keys of the schema survive, and
8521 8582 // every value takes the type of its default. Until 2.11.0 this was an
8522 8583 // array_replace_recursive() of the file over the defaults, so any key in
@@ -8952,9 +9013,29 @@
8952 9013 if ( ! current_user_can( 'manage_options' ) ) {
8953 9014 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
8954 9015 }
8955 9016
8956 - $file = isset( $_POST['file'] ) ? sanitize_text_field( wp_unslash( $_POST['file'] ) ) : '';
9017 + /*
9018 + * The path as the scan reported it, which is what the interface sends
9019 + * back. sanitize_text_field() is the wrong reader for it: it deletes
9020 + * every %XX octet and collapses repeated spaces, so a file named
9021 + * foto%20nueva.php was stored as fotonueva.php, the scan compared the
9022 + * list with the paths it finds one by one, never matched, and the
9023 + * warning came back (measured on 1 oct 2026 against 3.0.1). The four
9024 + * handlers of the ignore list read the path the same way.
9025 + *
9026 + * Known limits, found by the cross review, that only a file name with a
9027 + * stray < in it reaches. wp_strip_all_tags() shortens such a name at the
9028 + * < instead of refusing it, so the stored entry can match another file
9029 + * (a.php<b is stored as a.php), and the value ignore_file() returns is
9030 + * not read, so a refusal of its own is answered as success. The fix, if
9031 + * a site ever needs it, is to answer with an error when
9032 + * wp_strip_all_tags() changes what was sent and to check that return
9033 + * value. An entry the old reader stored for such a name (it wrote the
9034 + * < as &lt;) is listed but "Stop ignoring" cannot remove it; "Clear all
9035 + * ignored files" does.
9036 + */
9037 + $file = ( isset( $_POST['file'] ) && is_string( $_POST['file'] ) ) ? wp_strip_all_tags( wp_unslash( $_POST['file'] ) ) : '';
8957 9038
8958 9039 if ( empty( $file ) ) {
8959 9040 wp_send_json_error( __( 'No file specified.', 'vigilante' ) );
8960 9041 }
@@ -9007,9 +9088,9 @@
9007 9088 if ( ! current_user_can( 'manage_options' ) ) {
9008 9089 wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
9009 9090 }
9010 9091
9011 - $file = isset( $_POST['file'] ) ? sanitize_text_field( wp_unslash( $_POST['file'] ) ) : '';
9092 + $file = ( isset( $_POST['file'] ) && is_string( $_POST['file'] ) ) ? wp_strip_all_tags( wp_unslash( $_POST['file'] ) ) : '';
9012 9093
9013 9094 if ( empty( $file ) ) {
9014 9095 wp_send_json_error( __( 'No file specified.', 'vigilante' ) );
9015 9096 }
@@ -9042,9 +9123,9 @@
9042 9123 $files = array();
9043 9124 $locked = $this->critical_approval_locked();
9044 9125 $shared = $locked ? array( 'wp-config.php', '.htaccess' ) : array();
9045 9126 foreach ( $raw_files as $f ) {
9046 - $clean = sanitize_text_field( $f );
9127 + $clean = is_string( $f ) ? wp_strip_all_tags( $f ) : '';
9047 9128 // Same rule as ajax_ignore_file(): the two shared files when the
9048 9129 // network locks them, and Vigilant's own files always.
9049 9130 if ( '' !== $clean && ! in_array( $clean, $shared, true ) && ! Vigilante_Self_Integrity::is_own_file_path( $clean ) ) {
9050 9131 $files[] = $clean;
@@ -9111,9 +9192,9 @@
9111 9192 }
9112 9193
9113 9194 $files = array();
9114 9195 foreach ( $raw_files as $f ) {
9115 - $clean = sanitize_text_field( $f );
9196 + $clean = is_string( $f ) ? wp_strip_all_tags( $f ) : '';
9116 9197 if ( '' !== $clean ) {
9117 9198 $files[] = $clean;
9118 9199 }
9119 9200 }