PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 3.0.0
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v3.0.0
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 | vigilante.php +253 -7 2.11.73.0.0 View file →
@@ -2,9 +2,9 @@
2 2 /**
3 3 * Plugin Name: Vigilant - 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner…
4 4 * Plugin URI: https://servicios.ayudawp.com
5 5 * Description: Complete security solution for WordPress. Firewall, 2FA, security headers, login protection, file integrity monitoring, activity logging and more.
6 - * Version: 2.11.7
6 + * Version: 3.0.0
7 7 * Author: Fernando Tellado
8 8 * Author URI: https://ayudawp.com
9 9 * Text Domain: vigilante
10 10 * Requires at least: 6.2
@@ -23,9 +23,9 @@
23 23
24 24 /**
25 25 * Plugin constants
26 26 */
27 -define( 'VIGILANTE_VERSION', '2.11.7' );
27 +define( 'VIGILANTE_VERSION', '3.0.0' );
28 28 define( 'VIGILANTE_PLUGIN_FILE', __FILE__ );
29 29 define( 'VIGILANTE_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
30 30 define( 'VIGILANTE_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
31 31 define( 'VIGILANTE_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
@@ -136,8 +136,11 @@
136 136 require_once VIGILANTE_INCLUDES_DIR . 'class-activity-log.php';
137 137 require_once VIGILANTE_INCLUDES_DIR . 'class-audit-alerts.php';
138 138 require_once VIGILANTE_INCLUDES_DIR . 'class-file-integrity.php';
139 139 require_once VIGILANTE_INCLUDES_DIR . 'class-plugin-status.php';
140 + require_once VIGILANTE_INCLUDES_DIR . 'class-self-integrity-guidance.php';
141 + require_once VIGILANTE_INCLUDES_DIR . 'class-self-integrity.php';
142 + require_once VIGILANTE_INCLUDES_DIR . 'class-self-repair.php';
140 143 require_once VIGILANTE_INCLUDES_DIR . 'class-under-attack.php';
141 144 require_once VIGILANTE_INCLUDES_DIR . 'class-database-backup.php';
142 145 require_once VIGILANTE_INCLUDES_DIR . 'class-database-prefix.php';
143 146 require_once VIGILANTE_INCLUDES_DIR . 'class-security-analyzer.php';
@@ -158,8 +161,18 @@
158 161
159 162 // Post-Under Attack scan (one-shot, scheduled by Vigilante_Under_Attack::deactivate).
160 163 add_action( 'vigilante_under_attack_post_scan', 'vigilante_run_post_under_attack_scan' );
161 164
165 + // Self-protection: verify Vigilant's own files right after the WordPress
166 + // updater replaced them. Priority 10, ahead of the File Integrity handler
167 + // at 20, which skips Vigilant while the self-check is on. Registered outside
168 + // is_admin() because automatic updates run from cron and WP-CLI.
169 + add_action( 'upgrader_process_complete', 'vigilante_on_upgrader_process_complete', 10, 2 );
170 +
171 + // One click repair of Vigilant's own files (admin-post action).
172 + Vigilante_Self_Repair::init();
173 + add_filter( 'upgrader_post_install', 'vigilante_mark_upgrader_wrote', 10, 3 );
174 +
162 175 // Initialize core components only - modules will be initialized at init
163 176 add_action( 'init', 'vigilante_init_plugin', 1 );
164 177 }
165 178
@@ -203,8 +216,15 @@
203 216 */
204 217 public $activity_log;
205 218
206 219 /**
220 + * Self-protection instance, the only one that registers hooks
221 + *
222 + * @var Vigilante_Self_Integrity|null
223 + */
224 + public $self_integrity;
225 +
226 + /**
207 227 * Get single instance of the class
208 228 *
209 229 * @return Vigilante_Main
210 230 */
@@ -364,15 +384,45 @@
364 384
365 385 // User Security
366 386 if ( ! empty( $options['modules']['user_security'] ) ) {
367 387 new Vigilante_User_Security( $this->settings, $this->activity_log );
388 + } else {
389 + /*
390 + * Turning the module off must not quietly unlock the accounts it
391 + * already locked. A forced password reset and a registration waiting
392 + * for approval are marks written on somebody's account, with their
393 + * sessions already destroyed and the activity log saying they cannot
394 + * get in; until 2.11.10 both stopped being enforced the moment this
395 + * toggle went off and every one of those accounts logged in again
396 + * with its old password. Only the enforcing half is registered here.
397 + */
398 + new Vigilante_User_Security( $this->settings, $this->activity_log, true );
368 399 }
369 400
370 401 // Login Security
402 + $login_security = null;
403 +
371 404 if ( ! empty( $options['modules']['login_security'] ) ) {
372 405 $login_security = new Vigilante_Login_Security( $this->settings, $this->database, $this->activity_log );
373 -
374 - // Two-Factor Authentication (only if login security module is active)
406 + }
407 +
408 + /*
409 + * Two factor on a network is decided for the whole network, so it has to
410 + * be ENFORCED on the whole network too. Gating these two on this site's
411 + * module toggle left the last leg of the bypass open: the administrator
412 + * of any subsite can turn Login Security off on their own site, which
413 + * takes them out of the picture but not out of the network, and the
414 + * session cookie WordPress issues there is valid on every host of it. So
415 + * a login sent to that subsite registered no second factor check at all
416 + * and the cookie it handed back opened the main site. Reproduced over
417 + * HTTP by the second cross review of 2.11.10.
418 + *
419 + * Same reasoning, and the same shape, as the enforcement-only User
420 + * Security above: on a network the enforcing half is registered whatever
421 + * this site says. On a single site there is no other site to protect and
422 + * the toggle means what it says.
423 + */
424 + if ( is_multisite() || null !== $login_security ) {
375 425 new Vigilante_Two_Factor_Email( $this->settings, $this->database, $this->activity_log, $login_security );
376 426 new Vigilante_Two_Factor_TOTP( $this->settings, $this->database, $this->activity_log, $login_security );
377 427 }
378 428
@@ -433,8 +483,16 @@
433 483
434 484 // Under Attack mode - always loaded (independent of modules)
435 485 new Vigilante_Under_Attack( $this->settings, $this->activity_log );
436 486
487 + // Self-protection - always loaded, NOT gated by modules.file_integrity:
488 + // the version change and watchdog paths must stay alive with the File
489 + // Integrity module off. There is no setting to gate it: see
490 + // Vigilante_Self_Integrity::is_on(). Hooks are registered here and only here;
491 + // every other new of the class is a plain object.
492 + $this->self_integrity = new Vigilante_Self_Integrity( $this->settings, $this->activity_log );
493 + $this->self_integrity->init_hooks();
494 +
437 495 // Admin interface
438 496 if ( is_admin() ) {
439 497 new Vigilante_Admin( $this->settings, $this->database, $this->activity_log );
440 498 }
@@ -578,8 +636,10 @@
578 636 * backoff for something that is already being handled.
579 637 */
580 638 $locked = false;
581 639 $incomplete = false;
640 + $unreadable = false;
641 + $settled = array( 'locked', 'block_incomplete', 'read_failed' );
582 642
583 643 if ( $needs_protection_block ) {
584 644 require_once VIGILANTE_INCLUDES_DIR . 'class-htaccess-protection.php';
585 645 $result = ( new Vigilante_Htaccess_Protection( $this->settings ) )->apply_rules( true );
@@ -585,9 +645,10 @@
585 645 $result = ( new Vigilante_Htaccess_Protection( $this->settings ) )->apply_rules( true );
586 646 $code = is_wp_error( $result ) ? $result->get_error_code() : ( true === $result ? '' : 'unexpected_result' );
587 647 $locked = $locked || 'locked' === $code;
588 648 $incomplete = $incomplete || 'block_incomplete' === $code;
589 - $failed = $failed || ( '' !== $code && 'locked' !== $code && 'block_incomplete' !== $code );
649 + $unreadable = $unreadable || 'read_failed' === $code;
650 + $failed = $failed || ( '' !== $code && ! in_array( $code, $settled, true ) );
590 651 $rewrote = true;
591 652 }
592 653
593 654 if ( ! $locked && ! empty( $options['modules']['security_headers'] ) ) {
@@ -595,9 +656,10 @@
595 656 $result = ( new Vigilante_Security_Headers( $this->settings ) )->apply_rules( true );
596 657 $code = is_wp_error( $result ) ? $result->get_error_code() : ( true === $result ? '' : 'unexpected_result' );
597 658 $locked = $locked || 'locked' === $code;
598 659 $incomplete = $incomplete || 'block_incomplete' === $code;
599 - $failed = $failed || ( '' !== $code && 'locked' !== $code && 'block_incomplete' !== $code );
660 + $unreadable = $unreadable || 'read_failed' === $code;
661 + $failed = $failed || ( '' !== $code && ! in_array( $code, $settled, true ) );
600 662 $rewrote = true;
601 663 }
602 664
603 665 if ( $locked ) {
@@ -639,11 +701,28 @@
639 701 'warning'
640 702 );
641 703 }
642 704
705 + /*
706 + * Same for a .htaccess that PHP can write but not read: since 2.11.8 it is
707 + * left as it is rather than replaced by the Vigilant rules alone, and
708 + * that does not mend itself either. The first version of that fix left
709 + * it to the hourly retry, with a message about read only files; found by
710 + * the cross review of 2.11.8.
711 + */
712 + if ( $unreadable && $this->activity_log ) {
713 + $this->activity_log->log(
714 + 'system',
715 + 'server_rules_read_failed',
716 + __( 'The .htaccess rules were not rewritten after the update because PHP can write that file but cannot read it, and writing it without reading it would have removed every other rule in it. Let PHP read the file, then save the Firewall or Headers tab.', 'vigilante' ),
717 + array( 'version' => VIGILANTE_VERSION ),
718 + 'warning'
719 + );
720 + }
721 +
643 722 $this->mark_server_files_synced();
644 723
645 - if ( $rewrote && ! $incomplete && $this->activity_log ) {
724 + if ( $rewrote && ! $incomplete && ! $unreadable && $this->activity_log ) {
646 725 $this->activity_log->log(
647 726 'system',
648 727 'server_rules_refreshed',
649 728 sprintf(
@@ -730,8 +809,26 @@
730 809 }
731 810 }
732 811 }
733 812
813 + // Self-protection from cron, for sites nobody opens the admin of: the
814 + // watchdog of Vigilant's own scheduled events, and the check for a
815 + // version change made outside the WordPress updater (FTP, manual).
816 + // The version change goes first: it is the path that reports a downgrade
817 + // by email, and the watchdog's own daily check would otherwise get there
818 + // before it.
819 + if ( $this->self_integrity ) {
820 + if ( $this->self_integrity->is_enabled() ) {
821 + $this->self_integrity->detect_version_change();
822 + $this->self_integrity->run_watchdog();
823 + } else {
824 + // Switched off by code: the one line that still has to be
825 + // written, or an installation that stopped checking itself
826 + // would do it in silence.
827 + $this->self_integrity->audit_off_state();
828 + }
829 + }
830 +
734 831 // Log maintenance
735 832 $this->activity_log->log( 'system', 'maintenance', __( 'Daily maintenance completed', 'vigilante' ) );
736 833 }
737 834
@@ -838,8 +935,157 @@
838 935 }
839 936
840 937 $under_attack = new Vigilante_Under_Attack( $settings, $activity_log );
841 938 $under_attack->run_analyzer_scan( 'all' );
939 +}
940 +
941 +/**
942 + * Verify Vigilant's own files right after the WordPress updater replaced them
943 + *
944 + * Runs as the OLD code with the NEW files already on disk, so the class reads
945 + * everything from disk (the Version header, the manifest) and nothing from
946 + * constants in memory. The first update to 3.0.0 is not seen here, because the
947 + * code that receives the hook is 2.x: that one is covered by the 3.0.0
948 + * migration block of Vigilante_Admin::run_migrations().
949 + *
950 + * Plugin_Upgrader::upgrade() passes the updated plugin in 'plugin', and
951 + * bulk_upgrade() passes the list in 'plugins'; both are read. Replacing the
952 + * plugin by uploading its zip, from the screen or with `wp plugin install
953 + * --force`, goes through Plugin_Upgrader::install() instead, whose context
954 + * names no plugin (wp-admin/includes/class-plugin-upgrader.php, install()):
955 + * plugin_info() reads it from the folder that was written.
956 + *
957 + * The check itself waits for the end of the request (see
958 + * vigilante_verify_after_upgrade()): WP_Upgrader::run() fires this hook also
959 + * when the install failed, and restores the previous copy of the plugin on
960 + * shutdown, so checking here reported the half-moved folder of a failed update
961 + * as tampering, by email.
962 + *
963 + * @since 3.0.0
964 + *
965 + * @param WP_Upgrader|mixed $upgrader Upgrader instance.
966 + * @param array $hook_extra Update context.
967 + */
968 +function vigilante_on_upgrader_process_complete( $upgrader, $hook_extra ) {
969 + if ( ! is_array( $hook_extra ) || 'plugin' !== ( isset( $hook_extra['type'] ) ? $hook_extra['type'] : '' ) ) {
970 + return;
971 + }
972 +
973 + $action = isset( $hook_extra['action'] ) ? $hook_extra['action'] : '';
974 + $files = array();
975 + if ( 'update' === $action ) {
976 + $files = ( ! empty( $hook_extra['plugins'] ) && is_array( $hook_extra['plugins'] ) ) ? $hook_extra['plugins'] : array();
977 + if ( ! empty( $hook_extra['plugin'] ) ) {
978 + $files[] = $hook_extra['plugin'];
979 + }
980 + } elseif ( 'install' === $action && is_object( $upgrader ) && method_exists( $upgrader, 'plugin_info' ) ) {
981 + $installed = $upgrader->plugin_info();
982 + if ( is_string( $installed ) && '' !== $installed ) {
983 + $files[] = $installed;
984 + }
985 + } else {
986 + return;
987 + }
988 + if ( ! in_array( VIGILANTE_PLUGIN_BASENAME, array_map( 'strval', $files ), true ) ) {
989 + return;
990 + }
991 +
992 + // After WP_Upgrader::restore_temp_backup() (shutdown, priority 10) and
993 + // before WP_Upgrader::delete_temp_backup() (priority 100).
994 + if ( false === has_action( 'shutdown', 'vigilante_verify_after_upgrade' ) ) {
995 + add_action( 'shutdown', 'vigilante_verify_after_upgrade', 50 );
996 + }
997 +}
998 +
999 +/**
1000 + * The check after an update, at the end of the request
1001 + *
1002 + * By now a failed update has had its previous copy restored, so the files
1003 + * on disk are the ones the site will run. Hooked by
1004 + * vigilante_on_upgrader_process_complete(); a bulk update with Vigilant in
1005 + * the list hooks it once.
1006 + *
1007 + * @since 3.0.0
1008 + */
1009 +function vigilante_verify_after_upgrade() {
1010 + if ( ! class_exists( 'Vigilante_Settings' ) ) {
1011 + require_once VIGILANTE_INCLUDES_DIR . 'class-settings.php';
1012 + }
1013 + if ( ! class_exists( 'Vigilante_Self_Integrity' ) ) {
1014 + require_once VIGILANTE_INCLUDES_DIR . 'class-self-integrity-guidance.php';
1015 + require_once VIGILANTE_INCLUDES_DIR . 'class-self-integrity.php';
1016 + }
1017 +
1018 + $settings = new Vigilante_Settings();
1019 + $activity_log = null;
1020 + if ( class_exists( 'Vigilante_Activity_Log' ) && class_exists( 'Vigilante_Database' ) ) {
1021 + $database = new Vigilante_Database();
1022 + $activity_log = new Vigilante_Activity_Log( $settings, $database );
1023 + }
1024 +
1025 + $self_integrity = new Vigilante_Self_Integrity( $settings, $activity_log );
1026 + $self_integrity->handle_upgrader( vigilante_upgrader_wrote_files_on_disk() );
1027 +}
1028 +
1029 +/**
1030 + * Whether the files on disk are the ones the WordPress updater wrote in this request
1031 + *
1032 + * The marker of vigilante_mark_upgrader_wrote() says WordPress wrote the
1033 + * folder, not that the folder is still that copy at the end of the request:
1034 + * the automatic updater puts the previous copy back when the updated plugin
1035 + * breaks its loopback request, and a failed install is restored on shutdown.
1036 + * The manifest identifies the copy, so the updater is trusted only when the
1037 + * manifest on disk is the one it wrote.
1038 + *
1039 + * @since 3.0.0
1040 + *
1041 + * @return bool
1042 + */
1043 +function vigilante_upgrader_wrote_files_on_disk() {
1044 + $mark = isset( $GLOBALS['vigilante_upgrader_wrote'] ) ? $GLOBALS['vigilante_upgrader_wrote'] : null;
1045 + if ( ! is_array( $mark ) || ! array_key_exists( 'manifest', $mark ) ) {
1046 + return false;
1047 + }
1048 + return $mark['manifest'] === Vigilante_Self_Integrity::manifest_fingerprint_of( VIGILANTE_PLUGIN_DIR );
1049 +}
1050 +
1051 +/**
1052 + * Remember that the WordPress updater wrote Vigilant's folder in this request
1053 + *
1054 + * upgrader_post_install runs once per package, after the files are in place
1055 + * and before upgrader_process_complete. The hook that follows names every
1056 + * plugin of a bulk update, including the ones that were skipped, so the check
1057 + * after the update only trusts the updater when this marker says it really
1058 + * replaced the folder. Only a folder of that name in the plugins directory
1059 + * counts (a theme can have the same name), and the marker keeps the
1060 + * fingerprint of the manifest that was written, which
1061 + * vigilante_upgrader_wrote_files_on_disk() compares with the disk at the end.
1062 + *
1063 + * @since 3.0.0
1064 + *
1065 + * @param bool|WP_Error $response Installation response.
1066 + * @param array $hook_extra Extra arguments passed to hooked filters.
1067 + * @param array $result Installation result data.
1068 + * @return bool|WP_Error The response, unchanged.
1069 + */
1070 +function vigilante_mark_upgrader_wrote( $response, $hook_extra, $result ) {
1071 + if ( is_wp_error( $response ) || ! is_array( $result ) || ! isset( $result['destination_name'], $result['destination'], $result['local_destination'] ) ) {
1072 + return $response;
1073 + }
1074 + if ( dirname( VIGILANTE_PLUGIN_BASENAME ) !== $result['destination_name'] ) {
1075 + return $response;
1076 + }
1077 + if ( untrailingslashit( wp_normalize_path( (string) $result['local_destination'] ) ) !== untrailingslashit( wp_normalize_path( WP_PLUGIN_DIR ) ) ) {
1078 + return $response;
1079 + }
1080 + if ( ! class_exists( 'Vigilante_Self_Integrity' ) ) {
1081 + require_once VIGILANTE_INCLUDES_DIR . 'class-self-integrity-guidance.php';
1082 + require_once VIGILANTE_INCLUDES_DIR . 'class-self-integrity.php';
1083 + }
1084 + $GLOBALS['vigilante_upgrader_wrote'] = array(
1085 + 'manifest' => Vigilante_Self_Integrity::manifest_fingerprint_of( (string) $result['destination'] ),
1086 + );
1087 + return $response;
842 1088 }
843 1089
844 1090 /**
845 1091 * Plugin activation hook