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.php +18 -252 2.11.122.11.8 View file →
@@ -199,26 +199,8 @@
199 199 /**
200 200 * Run database migrations based on stored version
201 201 */
202 202 public function run_migrations() {
203 - /*
204 - * admin-ajax.php fires admin_init before it decides who is asking
205 - * (wp-admin/admin-ajax.php:45), so until 2.11.10 an anonymous POST to
206 - * admin-ajax.php with any action ran every pending migration. That is
207 - * not a read: the migrations rewrite wp-config.php through
208 - * apply_security_constants(), rewrite the root .htaccess, move user meta
209 - * of the whole network and can rebuild the file integrity baseline,
210 - * taking whatever is on disk as approved. Reproduced on 12 sep 2026 with
211 - * curl and no cookies, and found by the file-by-file review of 2.11.10.
212 - *
213 - * Migrations are maintenance for whoever administers the site, so they
214 - * wait for an administrator to load a screen. Nothing is lost by
215 - * waiting: every migration is idempotent and version gated.
216 - */
217 - if ( ! is_user_logged_in() || ! current_user_can( 'manage_options' ) ) {
218 - return;
219 - }
220 -
221 203 $db_version = get_option( 'vigilante_db_version', '0' );
222 204
223 205 // 1.2.3: Fix IP lists corrupted by sanitize_text_field stripping newlines
224 206 if ( version_compare( $db_version, '1.2.3', '<' ) ) {
@@ -482,153 +464,11 @@
482 464 $this->database->purge_for_2_11_0();
483 465
484 466 update_option( 'vigilante_db_version', '2.11.0' );
485 467 }
486 -
487 - /*
488 - * 2.11.9: clear the raw .htaccess copies that older versions left in
489 - * options, on the first admin load after the update. Uninstall already
490 - * removes them, but that only fires when the plugin is deleted, so a
491 - * site that keeps the plugin carried them until now. Three stores, each
492 - * a copy of a file that can hold secrets (a SetEnv token, an
493 - * Authorization header): the same exposure the wp.org review flagged as
494 - * 4.4, on the paths its fix did not reach.
495 - *
496 - * - vigilante_htaccess_history: up to five raw copies, by design, until
497 - * 2.11.8. The writer is gone, nothing reads it, so it is deleted.
498 - * - vigilante_htaccess_backup: the single rollback buffer, normally
499 - * cleared in the finally of each write; a copy only lingers if a write
500 - * crashed mid-operation. Nothing outside one write reads it, so a
501 - * leftover is deleted.
502 - * - vigilante_htaccess_pre_migration: still read by the header recovery,
503 - * but older versions stored the whole file where only our own block is
504 - * ever used. Truncated to that block, so the feature keeps working and
505 - * nothing outside our markers stays in the option.
506 - */
507 - if ( version_compare( $db_version, '2.11.9', '<' ) ) {
508 - delete_option( 'vigilante_htaccess_history' );
509 - delete_option( 'vigilante_htaccess_backup' );
510 -
511 - $snapshot = get_option( 'vigilante_htaccess_pre_migration' );
512 - if ( is_array( $snapshot ) && isset( $snapshot['content'] ) && '' !== (string) $snapshot['content'] ) {
513 - require_once VIGILANTE_INCLUDES_DIR . 'class-htaccess-recovery.php';
514 - $block = Vigilante_Htaccess_Recovery::get_raw_block();
515 -
516 - if ( '' === $block ) {
517 - delete_option( 'vigilante_htaccess_pre_migration' );
518 - } elseif ( $block !== $snapshot['content'] ) {
519 - $snapshot['content'] = $block;
520 - update_option( 'vigilante_htaccess_pre_migration', $snapshot, false );
521 - }
522 - }
523 -
524 - update_option( 'vigilante_db_version', '2.11.9' );
525 - }
526 -
527 - /*
528 - * 2.11.10: the pending-approval flag becomes one per site on a network.
529 - * Until 2.11.9 it was a single global user meta, so the queue was shared
530 - * across the whole network. Moving the key is not enough: the accounts
531 - * already waiting carry the old key, and reading only the new one would
532 - * let them log in. So they are moved here, each to the site it belongs
533 - * to, and the old key is removed only once the new one is written.
534 - */
535 - if ( version_compare( $db_version, '2.11.10', '<' ) ) {
536 - $this->migrate_pending_approval_per_site();
537 -
538 - update_option( 'vigilante_db_version', '2.11.10' );
539 - }
540 468 }
541 469
542 470 /**
543 - * Move the pending-approval flag of a network to a key per site
544 - *
545 - * Runs once for the whole network, not once per site: the data it moves is
546 - * global, so the guard is a network option and any site may be the one that
547 - * does it. On a single site the key does not change and there is nothing to
548 - * do.
549 - *
550 - * Each waiting account goes to its primary site, or to the only site it
551 - * belongs to; one that belongs to none goes to the main site rather than
552 - * nowhere, because losing the flag would silently approve it.
553 - *
554 - * @since 2.11.10
555 - */
556 - private function migrate_pending_approval_per_site() {
557 - global $wpdb;
558 -
559 - if ( ! is_multisite() ) {
560 - return;
561 - }
562 -
563 - if ( get_site_option( 'vigilante_pending_per_site_done' ) ) {
564 - return;
565 - }
566 -
567 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- One-off migration of the plugin's own user meta; the meta API has no "list every user with this key".
568 - $user_ids = $wpdb->get_col(
569 - $wpdb->prepare( "SELECT DISTINCT user_id FROM {$wpdb->usermeta} WHERE meta_key = %s", 'vigilante_pending_approval' )
570 - );
571 -
572 - foreach ( (array) $user_ids as $user_id ) {
573 - $user_id = (int) $user_id;
574 - if ( ! $user_id ) {
575 - continue;
576 - }
577 -
578 - $pending = get_user_meta( $user_id, 'vigilante_pending_approval', true );
579 - $since = get_user_meta( $user_id, 'vigilante_pending_since', true );
580 -
581 - /*
582 - * Every site the account belongs to, not its primary one. The global
583 - * flag does not say where the registration happened, and the first
584 - * version of this guessed the primary blog: an account that
585 - * registered on B while its primary was A came out pending on A and
586 - * free to log in on B, which is the very site it had never been
587 - * approved on. Found by the cross review of 2.11.10.
588 - *
589 - * Marking every site it belongs to fails closed instead: the account
590 - * stays blocked wherever it can log in, and shows up in the queue of
591 - * each of those sites so somebody can actually act on it. An account
592 - * that belongs to no site goes to the main one rather than nowhere,
593 - * because losing the flag would silently approve it.
594 - */
595 - /*
596 - * With $all true, because the default leaves out archived, spam and
597 - * deleted sites (wp-includes/user.php:1113-1117): a site archived on
598 - * the day this runs would lose the flag, and the account would walk
599 - * in unapproved the moment it was brought back. Found by the second
600 - * cross review of 2.11.10.
601 - */
602 - $blog_ids = array();
603 -
604 - foreach ( get_blogs_of_user( $user_id, true ) as $blog ) {
605 - if ( ! empty( $blog->userblog_id ) ) {
606 - $blog_ids[] = (int) $blog->userblog_id;
607 - }
608 - }
609 -
610 - if ( empty( $blog_ids ) ) {
611 - $blog_ids[] = (int) get_main_site_id();
612 - }
613 -
614 - foreach ( array_unique( $blog_ids ) as $blog_id ) {
615 - $prefix = $wpdb->get_blog_prefix( $blog_id );
616 -
617 - update_user_meta( $user_id, $prefix . 'vigilante_pending_approval', $pending );
618 - if ( '' !== $since && false !== $since ) {
619 - update_user_meta( $user_id, $prefix . 'vigilante_pending_since', $since );
620 - }
621 - }
622 -
623 - delete_user_meta( $user_id, 'vigilante_pending_approval' );
624 - delete_user_meta( $user_id, 'vigilante_pending_since' );
625 - }
626 -
627 - update_site_option( 'vigilante_pending_per_site_done', 1 );
628 - }
629 -
630 - /**
631 471 * Migration: Remove orphaned email fields from saved options
632 472 *
633 473 * v1.10.0 centralized notification recipients into email section.
634 474 * Old per-module notify_email fields and dead email section fields
@@ -896,32 +736,23 @@
896 736 if ( ! did_action( 'plugins_loaded' ) ) {
897 737 return 0;
898 738 }
899 739
900 - /*
901 - * Counted whether the feature is on or off. An account already waiting
902 - * stays blocked when it is switched off (see init_enforcement_hooks()),
903 - * so reporting zero there hid people who cannot log in and whom nobody
904 - * could see to approve. Found by the cross review of 2.11.10.
905 - */
740 + $registration_approval = $this->settings->get_section( 'user_security' );
741 + $approval_settings = $registration_approval['registration_approval'] ?? array();
742 +
743 + if ( empty( $approval_settings['enabled'] ) ) {
744 + return 0;
745 + }
906 746
907 747 // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Limited results in admin context.
908 - $args = array(
909 - 'meta_key' => Vigilante_User_Security::site_user_meta_key( 'vigilante_pending_approval' ),
748 + $pending_users = get_users( array(
749 + 'meta_key' => 'vigilante_pending_approval',
910 750 'meta_value' => '1',
911 751 'fields' => 'ID',
912 - );
752 + ) );
913 753 // phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value
914 754
915 - // Same query as Vigilante_User_Security::get_pending_users(), and for the
916 - // same reason: the meta key already scopes this to the site, and adding
917 - // core's membership filter on top hid the accounts that have no role yet.
918 - if ( is_multisite() ) {
919 - $args['blog_id'] = 0;
920 - }
921 -
922 - $pending_users = get_users( $args );
923 -
924 755 return count( $pending_users );
925 756 }
926 757
927 758 /**
@@ -3530,10 +3361,10 @@
3530 3361 <div id="vigilante-xff-chain-notice" class="notice notice-warning inline" style="margin:10px 0 16px;padding:8px 12px;">
3531 3362 <p style="margin:0;">
3532 3363 <?php
3533 3364 printf(
3534 - /* translators: 1: last address in the header, the one Vigilant reads, 2: first address in the header, which a visitor can write */
3535 - esc_html__( 'Your own request reaches the site with more than one public address in X-Forwarded-For. Vigilant reads the last one, %1$s, which is the one your proxy added, and not the first one, %2$s, which a visitor can write. If %1$s belongs to a CDN or a load balancer rather than to you, every visitor shares it for rate limiting, login lockouts and the IP lists: choose the header of that CDN in Visitor IP detection, such as CF-Connecting-IP for Cloudflare.', 'vigilante' ),
3365 + /* translators: 1: address Vigilant reads now, 2: address earlier versions read */
3366 + esc_html__( 'Your own request reaches the site with more than one public address in X-Forwarded-For. Vigilant reads the last one, %1$s, which is the one your proxy added; up to version 2.11.7 it read the first one, %2$s, which a visitor can write. If %1$s belongs to a CDN or a load balancer rather than to you, every visitor shares it for rate limiting, login lockouts and the IP lists: choose the header of that CDN in Visitor IP detection, such as CF-Connecting-IP for Cloudflare.', 'vigilante' ),
3536 3367 esc_html( $xff_readings['now'] ),
3537 3368 esc_html( $xff_readings['before'] )
3538 3369 );
3539 3370 ?>
@@ -3567,20 +3398,8 @@
3567 3398 </p>
3568 3399 </td>
3569 3400 </tr>
3570 3401 <tr>
3571 - <th scope="row"><label for="vigilante-f-firewall-trusted-proxies"><?php esc_html_e( 'Trusted proxy IPs', 'vigilante' ); ?></label></th>
3572 - <td>
3573 - <textarea id="vigilante-f-firewall-trusted-proxies" name="firewall[trusted_proxies]" rows="3" class="large-text code" placeholder="10.0.0.0/8&#10;192.168.1.1" <?php disabled( $vg_main_locked ); ?>><?php echo esc_textarea( implode( "\n", $options['trusted_proxies'] ?? array() ) ); ?></textarea>
3574 - <p class="description">
3575 - <?php esc_html_e( 'Only used with a forwarded header selected above. One IP or CIDR range per line: the addresses your proxy or load balancer connects from. The forwarded header is accepted only from these. Left empty, Vigilant accepts it from your own private network, and for Cloudflare from Cloudflare\'s own ranges automatically.', 'vigilante' ); ?>
3576 - <?php if ( in_array( $proxy_header, array( 'x-forwarded-for', 'x-real-ip' ), true ) && empty( $options['trusted_proxies'] ) ) : ?>
3577 - <br><strong><?php esc_html_e( 'The header above is trusted but no proxy IPs are set. If your proxy or load balancer connects from a public address, add it here, or the header is ignored for safety and every visitor is seen as that proxy.', 'vigilante' ); ?></strong>
3578 - <?php endif; ?>
3579 - </p>
3580 - </td>
3581 - </tr>
3582 - <tr>
3583 3402 <th scope="row"><label for="vigilante-f-firewall-ip-whitelist"><?php esc_html_e( 'IP Whitelist', 'vigilante' ); ?></label></th>
3584 3403 <td>
3585 3404 <textarea id="vigilante-f-firewall-ip-whitelist" name="firewall[ip_whitelist]" <?php disabled( $vg_main_locked ); ?> rows="4" class="large-text code" placeholder="192.168.1.50&#10;192.168.1.0/24&#10;192.168.1.*"><?php echo esc_textarea( implode( "\n", $options['ip_whitelist'] ?? array() ) ); ?></textarea>
3586 3405 <p class="description">
@@ -4333,9 +4152,9 @@
4333 4152 ?>
4334 4153 <div class="vigilante-settings-section" id="vigilante-headers-recovery">
4335 4154 <h2><?php esc_html_e( 'Recover your previous header settings', 'vigilante' ); ?></h2>
4336 4155 <p>
4337 - <?php esc_html_e( 'An earlier update reset this tab to factory values: the migration replaced the whole section instead of merging into it. Your server kept sending the right headers, because the .htaccess had not been rewritten yet, so Vigilant saved a copy of that file before touching it. These are the settings it found in that copy.', 'vigilante' ); ?>
4156 + <?php esc_html_e( 'Updating to 2.9.8 reset this tab to factory values: the migration replaced the whole section instead of merging into it. Your server kept sending the right headers, because the .htaccess had not been rewritten yet, so Vigilant saved a copy of that file before touching it. These are the settings it found in that copy.', 'vigilante' ); ?>
4338 4157 </p>
4339 4158 <?php if ( $taken ) : ?>
4340 4159 <p class="description">
4341 4160 <?php
@@ -4944,16 +4763,8 @@
4944 4763 <span class="vigilante-method-badge php"><?php esc_html_e( 'PHP', 'vigilante' ); ?></span>
4945 4764 </h2>
4946 4765 <p><?php esc_html_e( 'Limit the number of simultaneous sessions per user.', 'vigilante' ); ?></p>
4947 4766
4948 - <?php if ( Vigilante_User_Security::session_limit_is_network_wide() ) : ?>
4949 - <div class="notice notice-warning inline">
4950 - <p>
4951 - <?php esc_html_e( 'This limit does not apply on a network. WordPress keeps the sessions of an account for the whole network, not per site, so a limit set here would count and close the sessions that person opened on other sites, including an administrator session elsewhere. A network-wide session policy is planned; until then these settings are saved but not enforced.', 'vigilante' ); ?>
4952 - </p>
4953 - </div>
4954 - <?php endif; ?>
4955 -
4956 4767 <table class="form-table">
4957 4768 <tr>
4958 4769 <th scope="row"><?php esc_html_e( 'Enable Session Limits', 'vigilante' ); ?></th>
4959 4770 <td>
@@ -5293,17 +5104,9 @@
5293 5104 </div>
5294 5105
5295 5106 <!-- Pending Registrations -->
5296 5107 <?php
5297 - // Enforcement-only: this instance exists to read the queue, and the
5298 - // flag keeps it from registering the module's own hooks a second
5299 - // time. It is not inert, and saying it was would be a false comment:
5300 - // init_enforcement_hooks() does add its three filters again, on top
5301 - // of the ones already registered. They are idempotent (the same
5302 - // methods of an equivalent instance, deciding on the same user meta),
5303 - // so running them twice in an admin request changes nothing, which is
5304 - // why this is accepted rather than worked around.
5305 - $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log, true );
5108 + $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
5306 5109 $pending_users = $user_security->get_pending_users();
5307 5110 ?>
5308 5111 <div id="vigilante-section-users-pending" class="vigilante-tool-box vigilante-pending-users-section">
5309 5112 <h3>
@@ -5312,20 +5115,9 @@
5312 5115 <span class="vigilante-badge vigilante-badge-warning"><?php echo esc_html( count( $pending_users ) ); ?></span>
5313 5116 <?php endif; ?>
5314 5117 </h3>
5315 5118
5316 - <?php
5317 - /*
5318 - * The queue is shown whenever there is somebody in it, even with
5319 - * the feature off. Since 2.11.10 an account already waiting stays
5320 - * blocked when the feature is switched off, which is the point:
5321 - * turning a setting off must not quietly let in people an
5322 - * administrator decided not to approve. But hiding the table then
5323 - * left them locked out with no button anywhere to approve or
5324 - * reject them. Found by the cross review of 2.11.10.
5325 - */
5326 - ?>
5327 - <?php if ( empty( $registration['enabled'] ) && empty( $pending_users ) ) : ?>
5119 + <?php if ( empty( $registration['enabled'] ) ) : ?>
5328 5120 <p class="description">
5329 5121 <span class="dashicons dashicons-info" style="color: #72aee6;"></span>
5330 5122 <?php esc_html_e( 'Registration approval is disabled. Enable it in the settings above to require manual approval for new users.', 'vigilante' ); ?>
5331 5123 </p>
@@ -5346,9 +5138,9 @@
5346 5138 </tr>
5347 5139 </thead>
5348 5140 <tbody>
5349 5141 <?php foreach ( $pending_users as $pending_user ) :
5350 - $pending_since = get_user_meta( $pending_user->ID, Vigilante_User_Security::site_user_meta_key( 'vigilante_pending_since' ), true );
5142 + $pending_since = get_user_meta( $pending_user->ID, 'vigilante_pending_since', true );
5351 5143 ?>
5352 5144 <tr data-user-id="<?php echo esc_attr( $pending_user->ID ); ?>">
5353 5145 <td>
5354 5146 <?php echo get_avatar( $pending_user->ID, 32 ); ?>
@@ -7096,10 +6888,9 @@
7096 6888 }
7097 6889 }
7098 6890 }
7099 6891
7100 - $rejected_ips = array();
7101 - $rejected_proxies = array();
6892 + $rejected_ips = array();
7102 6893
7103 6894 // Handle modules
7104 6895 if ( 'modules' === $section && isset( $data['modules'] ) ) {
7105 6896 if ( ! isset( $saved_options['modules'] ) ) {
@@ -7124,9 +6915,9 @@
7124 6915 // went straight into the option. An entry the matcher can never
7125 6916 // match still sits in a security list looking like protection,
7126 6917 // so the ones that cannot match are dropped and reported back
7127 6918 // instead of being stored in silence.
7128 - $rejected_ips = $this->filter_ip_lists( $section, $processed, $rejected_proxies );
6919 + $rejected_ips = $this->filter_ip_lists( $section, $processed );
7129 6920
7130 6921 // Save the processed section
7131 6922 $saved_options[ $section ] = $processed;
7132 6923
@@ -7202,21 +6993,8 @@
7202 6993 implode( ', ', array_map( 'esc_html', $rejected_ips ) )
7203 6994 );
7204 6995 }
7205 6996
7206 - if ( ! empty( $rejected_proxies ) ) {
7207 - $message .= ' ' . sprintf(
7208 - /* translators: %s: comma separated list of the trusted proxy entries that were not saved. */
7209 - _n(
7210 - 'A trusted proxy must be an exact IP or a CIDR range, not a wildcard, so this entry was not saved: %s',
7211 - 'A trusted proxy must be an exact IP or a CIDR range, not a wildcard, so these entries were not saved: %s',
7212 - count( $rejected_proxies ),
7213 - 'vigilante'
7214 - ),
7215 - implode( ', ', array_map( 'esc_html', $rejected_proxies ) )
7216 - );
7217 - }
7218 -
7219 6997 wp_send_json_success( $message );
7220 6998 }
7221 6999
7222 7000 /**
@@ -7227,21 +7005,9 @@
7227 7005 * @param string $section Section being saved.
7228 7006 * @param array $processed Section data, edited in place.
7229 7007 * @return array Entries that were dropped, for the message back to the user.
7230 7008 */
7231 - private function filter_ip_lists( $section, &$processed, &$rejected_proxies = array() ) {
7232 - $rejected_proxies = array();
7233 -
7234 - // Trusted proxies feed an identity decision, so only exact addresses and
7235 - // CIDR ranges belong there: a wildcard is stripped with its own message,
7236 - // never stored looking effective. The matcher ignores it anyway (see
7237 - // Vigilante_IP_Utils::in_list_ip_or_cidr), this stops it persisting.
7238 - if ( 'firewall' === $section && isset( $processed['trusted_proxies'] ) && is_array( $processed['trusted_proxies'] ) ) {
7239 - $split = Vigilante_IP_Utils::split_list_ip_or_cidr( $processed['trusted_proxies'] );
7240 - $processed['trusted_proxies'] = $split['valid'];
7241 - $rejected_proxies = $split['rejected'];
7242 - }
7243 -
7009 + private function filter_ip_lists( $section, &$processed ) {
7244 7010 $lists = array(
7245 7011 'firewall' => array( 'ip_whitelist', 'ip_blacklist' ),
7246 7012 'login_security' => array( 'ip_whitelist' ),
7247 7013 );